oauthlint 0.3.0 → 0.5.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/README.md +28 -188
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +68 -7
- package/dist/cli.js.map +1 -1
- package/dist/commands/baseline.d.ts +21 -0
- package/dist/commands/baseline.d.ts.map +1 -0
- package/dist/commands/baseline.js +50 -0
- package/dist/commands/baseline.js.map +1 -0
- package/dist/commands/scan.d.ts +8 -1
- package/dist/commands/scan.d.ts.map +1 -1
- package/dist/commands/scan.js +47 -0
- package/dist/commands/scan.js.map +1 -1
- package/dist/core/baseline.d.ts +102 -0
- package/dist/core/baseline.d.ts.map +1 -0
- package/dist/core/baseline.js +223 -0
- package/dist/core/baseline.js.map +1 -0
- package/dist/core/update-notifier.d.ts +61 -0
- package/dist/core/update-notifier.d.ts.map +1 -0
- package/dist/core/update-notifier.js +267 -0
- package/dist/core/update-notifier.js.map +1 -0
- package/dist/formatters/html.d.ts +50 -0
- package/dist/formatters/html.d.ts.map +1 -0
- package/dist/formatters/html.js +308 -0
- package/dist/formatters/html.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -2,15 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
# oauthlint
|
|
4
4
|
|
|
5
|
-
**Catch the OAuth / OIDC / JWT anti-patterns AI coding tools systematically produce.**
|
|
5
|
+
**Catch the OAuth / OIDC / JWT / session / CORS anti-patterns AI coding tools systematically produce.**
|
|
6
6
|
|
|
7
|
-
A curated, multi-language Semgrep rule pack · JS/TS · Python · Go · Java · Rust
|
|
7
|
+
A curated, multi-language Semgrep rule pack · JS/TS · Python · Go · Java · Rust · CLI + GitHub Action + VS Code · free & MIT
|
|
8
8
|
|
|
9
9
|
[](https://www.npmjs.com/package/oauthlint)
|
|
10
10
|
[](https://www.npmjs.com/package/oauthlint)
|
|
11
11
|
[](https://github.com/Auspeo/oauthlint/actions/workflows/ci.yml)
|
|
12
12
|
[](https://opensource.org/licenses/MIT)
|
|
13
13
|
[](https://oauthlint.dev)
|
|
14
|
+
[](https://semgrep.dev)
|
|
14
15
|
|
|
15
16
|
</div>
|
|
16
17
|
|
|
@@ -20,58 +21,10 @@ npx oauthlint scan ./src
|
|
|
20
21
|
|
|
21
22
|
> Requires [Semgrep](https://semgrep.dev/docs/getting-started/) on the machine running the scan (`pipx install semgrep` or `brew install semgrep`). The CLI invokes it under the hood and normalises the output for humans and CI.
|
|
22
23
|
|
|
23
|
-
📖 **Full
|
|
24
|
+
📖 **Full docs & rule catalogue → [oauthlint.dev/docs](https://oauthlint.dev/docs)** · 🔬 **the research behind it → [oauthlint.dev/research](https://oauthlint.dev/research)**
|
|
24
25
|
|
|
25
26
|
---
|
|
26
27
|
|
|
27
|
-
## The problem
|
|
28
|
-
|
|
29
|
-
LLM coding assistants — Cursor, Claude, GitHub Copilot, Gemini — ship the *same*
|
|
30
|
-
OAuth/JWT mistakes across every project they touch:
|
|
31
|
-
|
|
32
|
-
- a JWT verified with `alg: none` accepted
|
|
33
|
-
- `client_secret` hard-coded in source
|
|
34
|
-
- an OAuth flow with no `state` and no PKCE
|
|
35
|
-
- a token written to `localStorage`, readable by any XSS
|
|
36
|
-
- `redirect_uri` allow-listed with a `*` wildcard
|
|
37
|
-
- a `/login` POST with no rate limiting
|
|
38
|
-
- a password persisted in plaintext
|
|
39
|
-
- `Math.random()` used for a CSRF token
|
|
40
|
-
|
|
41
|
-
oauthlint is the layer between generic SAST and an enterprise IAM program:
|
|
42
|
-
**free, focused, developer-first.** Every finding names the rule, the exact
|
|
43
|
-
`file:line`, *why* it's dangerous, and *how* to fix it — with CWE/OWASP mappings.
|
|
44
|
-
|
|
45
|
-
<div align="center">
|
|
46
|
-
|
|
47
|
-

|
|
48
|
-
|
|
49
|
-
</div>
|
|
50
|
-
|
|
51
|
-
## Why oauthlint, and not just Semgrep?
|
|
52
|
-
|
|
53
|
-
Honest answer: nothing stops you from writing these rules yourself. Semgrep is
|
|
54
|
-
open source — it's the engine we run — and a capable engineer could reproduce a
|
|
55
|
-
lot of this. There's no technical moat and we won't pretend otherwise. What
|
|
56
|
-
oauthlint gives you is the work most people never do:
|
|
57
|
-
|
|
58
|
-
- **Low false positives, validated against real auth libraries.** The rules are
|
|
59
|
-
run against `jose`, NextAuth, PyJWT, Authlib, `golang/oauth2`, `oauth2-rs`,
|
|
60
|
-
Spring Security and more. Anything that fires on mature library source goes to
|
|
61
|
-
a triage queue, not to you — validated across thousands of files of real
|
|
62
|
-
auth-library source, with **zero false positives** on the clean libraries
|
|
63
|
-
([validation report](https://oauthlint.dev/VALIDATION)).
|
|
64
|
-
- **One coherent product across every language it covers.** Same concepts, same ID scheme,
|
|
65
|
-
same docs — not a patchwork of community rules with mismatched styles.
|
|
66
|
-
- **Every finding teaches.** All 90 rules link to a fix page with CWE and OWASP
|
|
67
|
-
mappings. It's a lesson, not a grep hit.
|
|
68
|
-
- **The angle the registry doesn't have:** oauthlint specifically targets the
|
|
69
|
-
auth bugs AI coding tools ship on repeat, encoded in each rule's
|
|
70
|
-
`llm-prevalence` metadata and measured by a [reproducible benchmark](https://github.com/Auspeo/oauthlint/tree/main/benchmark).
|
|
71
|
-
|
|
72
|
-
Use oauthlint when you'd rather not write and maintain an auth rule pack
|
|
73
|
-
yourself. That's the whole pitch.
|
|
74
|
-
|
|
75
28
|
## Quick start
|
|
76
29
|
|
|
77
30
|
```bash
|
|
@@ -81,162 +34,49 @@ npx oauthlint scan ./src
|
|
|
81
34
|
# fail CI on HIGH severity and above
|
|
82
35
|
npx oauthlint scan ./src --fail-on HIGH
|
|
83
36
|
|
|
84
|
-
#
|
|
85
|
-
npx oauthlint scan ./src --json
|
|
86
|
-
|
|
87
|
-
# GitHub Code Scanning
|
|
37
|
+
# GitHub Code Scanning (SARIF) or a shareable HTML audit report
|
|
88
38
|
npx oauthlint scan ./src --format sarif > oauthlint.sarif
|
|
89
|
-
|
|
90
|
-
# auto-apply safe fixes (e.g. cookie flags)
|
|
91
|
-
npx oauthlint scan ./src --fix
|
|
92
|
-
|
|
93
|
-
# scan several explicit paths (e.g. a list of changed files)
|
|
94
|
-
npx oauthlint scan src/auth.ts src/session.ts
|
|
95
|
-
|
|
96
|
-
# scan only what changed vs the default branch (fast, incremental)
|
|
97
|
-
npx oauthlint scan --diff
|
|
98
|
-
|
|
99
|
-
# scan only git-staged files (ideal in a pre-commit hook)
|
|
100
|
-
npx oauthlint scan --staged
|
|
39
|
+
npx oauthlint scan ./src --format html > report.html
|
|
101
40
|
```
|
|
102
41
|
|
|
103
|
-
|
|
42
|
+
Scan only what changed for fast pre-commit hooks and editors with `--diff` / `--staged`, or adopt on a large repo with a [baseline](https://oauthlint.dev/docs/baseline) (`oauthlint baseline ./src` then `scan --baseline`) so you're alerted on **new** findings only. Other commands: `list`, `init`, `doctor`. Run `oauthlint --help` or see the [full CLI reference](https://oauthlint.dev/docs/cli).
|
|
104
43
|
|
|
105
|
-
|
|
106
|
-
keystroke is wasteful. OAuthLint can scan **only the files that changed**:
|
|
107
|
-
|
|
108
|
-
```bash
|
|
109
|
-
# files changed vs the merge-base with the repo's default branch
|
|
110
|
-
oauthlint scan --diff
|
|
111
|
-
|
|
112
|
-
# files changed vs an explicit ref (branch, tag, or commit)
|
|
113
|
-
oauthlint scan --diff origin/main
|
|
114
|
-
oauthlint scan --diff HEAD~5
|
|
44
|
+
## What it catches
|
|
115
45
|
|
|
116
|
-
|
|
117
|
-
oauthlint scan --staged --fail-on HIGH
|
|
118
|
-
```
|
|
46
|
+
LLM coding assistants — Cursor, Claude, Copilot, Gemini — ship the *same* auth bugs across every project: a JWT accepted with `alg: none`, a hard-coded `client_secret`, an OAuth flow with no `state`/PKCE, a token in `localStorage`, a `*` wildcard `redirect_uri`, an unrate-limited `/login`, a plaintext password, `Math.random()` for a CSRF token.
|
|
119
47
|
|
|
120
|
-
|
|
48
|
+
- **100+ rules** across **JS/TS · Python · Go · Java · Rust**, each mapped to CWE/OWASP with a fix page — a lesson, not a grep hit.
|
|
49
|
+
- **Dataflow (taint) analysis** — beyond pattern-matching, the pack traces untrusted input through to dangerous sinks to catch **open-redirect** and **SSRF**.
|
|
50
|
+
- **HTML report** — `scan --format html` renders a self-contained, offline, no-JavaScript audit you can email or attach to a PR.
|
|
51
|
+
- Plus **SARIF** for Code Scanning, `--fix` for safe auto-fixes, incremental `--diff`/`--staged`, and a [baseline](https://oauthlint.dev/docs/baseline) for existing codebases.
|
|
121
52
|
|
|
122
|
-
|
|
123
|
-
repo's default branch (`origin/HEAD` → `origin/main` → `origin/master`, falling
|
|
124
|
-
back to `HEAD` when there's no remote). The change set is everything Added /
|
|
125
|
-
Copied / Modified / Renamed since that base, plus uncommitted work-tree and
|
|
126
|
-
staged changes, plus new untracked files. Deleted files are skipped.
|
|
127
|
-
- **`--staged`** — only the staged set (`git diff --cached`), the same files a
|
|
128
|
-
commit is about to capture.
|
|
129
|
-
- Explicit path args (`oauthlint scan a.ts b.ts`) scan exactly those paths.
|
|
53
|
+
👉 **Browse the always-current catalogue at [oauthlint.dev/rules](https://oauthlint.dev/rules/).**
|
|
130
54
|
|
|
131
|
-
|
|
132
|
-
lockfiles, images, …) are filtered out automatically, and an **empty change set
|
|
133
|
-
exits `0`** with a `No changed files to scan.` note — it never fails your commit.
|
|
134
|
-
Outside a git repository, `--diff` / `--staged` print a clear error and exit `2`
|
|
135
|
-
instead of crashing. Git is always invoked with arguments passed as an array
|
|
136
|
-
(never a shell string), so a ref name can't inject a shell command.
|
|
55
|
+
## Use directly with Semgrep — no install
|
|
137
56
|
|
|
138
|
-
|
|
57
|
+
Already have [Semgrep](https://semgrep.dev)? Run the full pack with one command, no config file:
|
|
139
58
|
|
|
140
59
|
```bash
|
|
141
|
-
|
|
142
|
-
npx oauthlint scan --staged --fail-on HIGH
|
|
60
|
+
semgrep --config https://oauthlint.dev/r/oauthlint.yaml ./src
|
|
143
61
|
```
|
|
144
62
|
|
|
145
|
-
|
|
63
|
+
Per-language bundles exist too (`oauthlint-python.yaml`, `oauthlint-go.yaml`, …). That URL is always the latest pack; for a pinned ruleset in CI, use this CLI (`npx oauthlint@<version> scan`) or vendor [`oauthlint-rules`](https://www.npmjs.com/package/oauthlint-rules). See [the Semgrep docs](https://oauthlint.dev/docs/semgrep).
|
|
146
64
|
|
|
147
|
-
|
|
148
|
-
oauthlint scan [paths...] Scan files/directories (default: current dir)
|
|
149
|
-
oauthlint scan a.ts b.ts Scan an explicit list of paths
|
|
150
|
-
oauthlint scan --diff [ref] Scan only files changed vs a git ref
|
|
151
|
-
oauthlint scan --staged Scan only git-staged files (pre-commit)
|
|
152
|
-
oauthlint scan --json Emit machine-readable JSON
|
|
153
|
-
oauthlint scan --format sarif Emit SARIF for GitHub Code Scanning
|
|
154
|
-
oauthlint scan --severity HIGH Only show findings ≥ HIGH
|
|
155
|
-
oauthlint scan --fail-on off Never fail the build (CI dry-run)
|
|
156
|
-
oauthlint scan --fix Auto-apply safe fixes
|
|
157
|
-
oauthlint list List every shipped rule
|
|
158
|
-
oauthlint list --json Same, as JSON
|
|
159
|
-
oauthlint init Generate .oauthlintrc.yml at cwd
|
|
160
|
-
oauthlint init --force Overwrite an existing config
|
|
161
|
-
oauthlint doctor Check your setup (Semgrep, config, …)
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
## Use it in CI
|
|
165
|
-
|
|
166
|
-
Run the CLI in a workflow and upload SARIF to **GitHub Code Scanning** so
|
|
167
|
-
findings appear inline on the PR:
|
|
168
|
-
|
|
169
|
-
```yaml
|
|
170
|
-
# .github/workflows/oauthlint.yml
|
|
171
|
-
jobs:
|
|
172
|
-
oauthlint:
|
|
173
|
-
runs-on: ubuntu-latest
|
|
174
|
-
steps:
|
|
175
|
-
- uses: actions/checkout@v4
|
|
176
|
-
- run: pipx install semgrep # the engine oauthlint runs
|
|
177
|
-
- run: npx oauthlint scan ./src --format sarif > oauthlint.sarif
|
|
178
|
-
- uses: github/codeql-action/upload-sarif@v3
|
|
179
|
-
with:
|
|
180
|
-
sarif_file: oauthlint.sarif
|
|
181
|
-
```
|
|
182
|
-
|
|
183
|
-
Or just fail the build on HIGH findings: `npx oauthlint scan ./src --fail-on HIGH`.
|
|
184
|
-
|
|
185
|
-
> A dedicated **GitHub Action** and a **VS Code extension** (inline diagnostics +
|
|
186
|
-
> Quick Fix suppressions) are on the way — follow the [repo](https://github.com/Auspeo/oauthlint) for releases.
|
|
187
|
-
|
|
188
|
-
## What it catches
|
|
189
|
-
|
|
190
|
-
Rules across OAuth 2.0, OIDC, JWT, cookies, CORS, secrets and session hygiene —
|
|
191
|
-
each mapped to CWE & OWASP, each with a documentation page. Languages covered
|
|
192
|
-
today (more on the way):
|
|
193
|
-
|
|
194
|
-
| Language | Libraries |
|
|
195
|
-
|----------|-----------|
|
|
196
|
-
| JavaScript / TypeScript | jose, jsonwebtoken, NextAuth, express, … |
|
|
197
|
-
| Python | PyJWT, Authlib, requests, Flask, Django |
|
|
198
|
-
| Java | Spring Security, jjwt, nimbus-jose-jwt |
|
|
199
|
-
| Go | golang-jwt, crypto/tls, net/http |
|
|
200
|
-
| Rust | jsonwebtoken, reqwest, actix/tower |
|
|
201
|
-
|
|
202
|
-
👉 **Browse the full, always-current catalogue at [oauthlint.dev/rules](https://oauthlint.dev/rules/).**
|
|
203
|
-
|
|
204
|
-
## Configuration
|
|
205
|
-
|
|
206
|
-
Generate a `.oauthlintrc.yml` at your repo root with `oauthlint init`:
|
|
207
|
-
|
|
208
|
-
```yaml
|
|
209
|
-
version: 1
|
|
210
|
-
include:
|
|
211
|
-
- "src/**/*.{ts,tsx,js,jsx}"
|
|
212
|
-
exclude:
|
|
213
|
-
- "**/*.test.ts"
|
|
214
|
-
rules:
|
|
215
|
-
auth.cookie.no-samesite: warn
|
|
216
|
-
auth.session.id-in-url: off
|
|
217
|
-
failOn: HIGH
|
|
218
|
-
```
|
|
65
|
+
## Why oauthlint, and not just Semgrep?
|
|
219
66
|
|
|
220
|
-
|
|
67
|
+
Honest answer: nothing stops you writing these rules yourself — Semgrep is open source and it's the engine we run. There's no technical moat. What oauthlint gives you is the work most people never do:
|
|
221
68
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
```
|
|
69
|
+
- **Low false positives, validated against real auth libraries** — `jose`, NextAuth, PyJWT, Authlib, `golang/oauth2`, `oauth2-rs`, Spring Security and more. Anything that fires on mature library source goes to a triage queue, not to you ([validation report](https://oauthlint.dev/VALIDATION)).
|
|
70
|
+
- **One coherent product across every language** — same concepts, same ID scheme, same docs; not a patchwork of community rules.
|
|
71
|
+
- **The angle the registry doesn't have** — it targets the auth bugs AI tools ship on repeat, encoded in each rule's `llm-prevalence` metadata and measured by a reproducible benchmark ([the research](https://oauthlint.dev/research)).
|
|
226
72
|
|
|
227
|
-
|
|
228
|
-
the next reviewer needs to see exactly which lines opted out, and why.
|
|
73
|
+
Use oauthlint when you'd rather not write and maintain an auth rule pack yourself. That's the whole pitch.
|
|
229
74
|
|
|
230
|
-
##
|
|
75
|
+
## Also available
|
|
231
76
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
| `0` | No finding at or above the `--fail-on` threshold |
|
|
235
|
-
| `1` | At least one **HIGH** finding |
|
|
236
|
-
| `2` | At least one **CRITICAL** finding, or a scan whose output could not be parsed (it never silently exits clean) |
|
|
237
|
-
| `127` | Semgrep is not installed |
|
|
77
|
+
- **GitHub Action** — `Auspeo/oauthlint/action@v1`, Docker-based (any language), with inline PR annotations + a job summary. [Docs](https://github.com/Auspeo/oauthlint/tree/main/action).
|
|
78
|
+
- **VS Code / Cursor / Windsurf** — [oauthlint](https://marketplace.visualstudio.com/items?itemName=auspeo.oauthlint-vscode) on the VS Code Marketplace and [OpenVSX](https://open-vsx.org/extension/auspeo/oauthlint-vscode): inline diagnostics on save, a status-bar finding count, and Quick Fix suppressions.
|
|
238
79
|
|
|
239
80
|
## License
|
|
240
81
|
|
|
241
|
-
MIT — see [LICENSE](https://github.com/Auspeo/oauthlint/blob/main/LICENSE).
|
|
242
|
-
Built and maintained by [Auspeo](https://github.com/Auspeo).
|
|
82
|
+
MIT — see [LICENSE](https://github.com/Auspeo/oauthlint/blob/main/LICENSE). Built and maintained by [Auspeo](https://github.com/Auspeo).
|
package/dist/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAyEpC,wBAAsB,YAAY,IAAI,OAAO,CAAC,OAAO,CAAC,CA2HrD"}
|
package/dist/cli.js
CHANGED
|
@@ -3,10 +3,12 @@ import { dirname, resolve } from 'node:path';
|
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import { Command } from 'commander';
|
|
5
5
|
import pc from 'picocolors';
|
|
6
|
+
import { runBaseline } from './commands/baseline.js';
|
|
6
7
|
import { runDoctor } from './commands/doctor.js';
|
|
7
8
|
import { runInit } from './commands/init.js';
|
|
8
9
|
import { runList } from './commands/list.js';
|
|
9
10
|
import { runScan } from './commands/scan.js';
|
|
11
|
+
import { maybeNotifyUpdate } from './core/update-notifier.js';
|
|
10
12
|
import { SEVERITIES } from './types.js';
|
|
11
13
|
/**
|
|
12
14
|
* Exit only once buffered stdout/stderr have drained.
|
|
@@ -28,6 +30,21 @@ async function exitAfterFlush(code) {
|
|
|
28
30
|
await Promise.all([flush(process.stdout), flush(process.stderr)]);
|
|
29
31
|
process.exit(code);
|
|
30
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* Print the "update available" notice (when allowed) AFTER the command's normal
|
|
35
|
+
* output, then exit once everything is flushed. The notifier is fire-and-forget
|
|
36
|
+
* and self-suppressing: it never blocks, never touches stdout, and is silent
|
|
37
|
+
* under `--json`/`--format sarif`, in CI, when piped, when `NO_UPDATE_NOTIFIER`
|
|
38
|
+
* is set, or when `--no-update-check` is passed.
|
|
39
|
+
*/
|
|
40
|
+
async function finishWithNotice(code, ctx) {
|
|
41
|
+
await maybeNotifyUpdate({
|
|
42
|
+
currentVersion: ctx.version,
|
|
43
|
+
machineReadable: ctx.machineReadable,
|
|
44
|
+
disabled: !ctx.updateCheck,
|
|
45
|
+
});
|
|
46
|
+
return exitAfterFlush(code);
|
|
47
|
+
}
|
|
31
48
|
async function readPackageVersion() {
|
|
32
49
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
33
50
|
// dist/cli.js → ../package.json (when published)
|
|
@@ -53,19 +70,24 @@ export async function buildProgram() {
|
|
|
53
70
|
program
|
|
54
71
|
.name('oauthlint')
|
|
55
72
|
.description('OAuthLint — catch the OAuth/OIDC/JWT anti-patterns AI coding tools systematically produce.')
|
|
56
|
-
.version(version, '-v, --version')
|
|
73
|
+
.version(version, '-v, --version')
|
|
74
|
+
.option('--no-update-check', 'Do not check npm for a newer oauthlint version');
|
|
75
|
+
// Resolve the global update-check opt-out once, lazily, from the root program.
|
|
76
|
+
// Commander sets `updateCheck: false` when `--no-update-check` is given.
|
|
77
|
+
const updateCheckEnabled = () => program.opts().updateCheck !== false;
|
|
57
78
|
program
|
|
58
79
|
.command('scan')
|
|
59
80
|
.argument('[paths...]', 'One or more files/directories to scan', ['.'])
|
|
60
81
|
.description('Scan files or directories for auth misconfigurations')
|
|
61
82
|
.option('--json', 'Emit JSON (shortcut for --format json)')
|
|
62
|
-
.option('--format <fmt>', 'Output format: pretty | json | sarif', parseFormat)
|
|
83
|
+
.option('--format <fmt>', 'Output format: pretty | json | sarif | html', parseFormat)
|
|
63
84
|
.option('--severity <level>', 'Only emit findings ≥ this severity', parseSeverity)
|
|
64
85
|
.option('--fail-on <level>', 'Process exits non-zero if any finding ≥ this severity', parseFailOn)
|
|
65
86
|
.option('--diff [ref]', 'Scan only files changed vs a git ref (default: merge-base with the default branch)')
|
|
66
87
|
.option('--staged', 'Scan only git-staged files (useful for pre-commit hooks)')
|
|
67
88
|
.option('--rules-dir <path>', 'Override the bundled rules directory')
|
|
68
89
|
.option('--fix', 'Apply auto-fixes (rewrites source in place where possible)')
|
|
90
|
+
.option('--baseline [file]', 'Suppress findings already in a baseline file; report only NEW findings (default: .oauthlint-baseline.json)')
|
|
69
91
|
.action(async (paths, opts) => {
|
|
70
92
|
const code = await runScan({
|
|
71
93
|
paths,
|
|
@@ -77,29 +99,68 @@ export async function buildProgram() {
|
|
|
77
99
|
failOn: opts.failOn,
|
|
78
100
|
rulesDir: opts.rulesDir,
|
|
79
101
|
fix: opts.fix,
|
|
102
|
+
baseline: opts.baseline,
|
|
103
|
+
});
|
|
104
|
+
const machineReadable = opts.json === true ||
|
|
105
|
+
opts.format === 'json' ||
|
|
106
|
+
opts.format === 'sarif' ||
|
|
107
|
+
opts.format === 'html';
|
|
108
|
+
await finishWithNotice(code, { version, machineReadable, updateCheck: updateCheckEnabled() });
|
|
109
|
+
});
|
|
110
|
+
program
|
|
111
|
+
.command('baseline')
|
|
112
|
+
.argument('[paths...]', 'One or more files/directories to scan', ['.'])
|
|
113
|
+
.description('Scan and write a baseline of current findings (for adopting on an existing codebase)')
|
|
114
|
+
.option('-o, --output <file>', 'Where to write the baseline JSON', '.oauthlint-baseline.json')
|
|
115
|
+
.option('--rules-dir <path>', 'Override the bundled rules directory')
|
|
116
|
+
.action(async (paths, opts) => {
|
|
117
|
+
const code = await runBaseline({
|
|
118
|
+
paths,
|
|
119
|
+
output: opts.output,
|
|
120
|
+
rulesDir: opts.rulesDir,
|
|
121
|
+
});
|
|
122
|
+
// baseline writes a JSON file but its stdout is a human summary.
|
|
123
|
+
await finishWithNotice(code, {
|
|
124
|
+
version,
|
|
125
|
+
machineReadable: false,
|
|
126
|
+
updateCheck: updateCheckEnabled(),
|
|
80
127
|
});
|
|
81
|
-
await exitAfterFlush(code);
|
|
82
128
|
});
|
|
83
129
|
program
|
|
84
130
|
.command('list')
|
|
85
131
|
.description('List every rule the current install ships with')
|
|
86
132
|
.option('--json', 'Emit JSON instead of pretty output')
|
|
87
133
|
.action(async (opts) => {
|
|
88
|
-
|
|
134
|
+
const code = await runList({ json: opts.json });
|
|
135
|
+
await finishWithNotice(code, {
|
|
136
|
+
version,
|
|
137
|
+
machineReadable: opts.json === true,
|
|
138
|
+
updateCheck: updateCheckEnabled(),
|
|
139
|
+
});
|
|
89
140
|
});
|
|
90
141
|
program
|
|
91
142
|
.command('init')
|
|
92
143
|
.description('Generate a .oauthlintrc.yml at the current directory')
|
|
93
144
|
.option('-f, --force', 'Overwrite an existing config file')
|
|
94
145
|
.action(async (opts) => {
|
|
95
|
-
|
|
146
|
+
const code = await runInit({ cwd: process.cwd(), force: opts.force });
|
|
147
|
+
await finishWithNotice(code, {
|
|
148
|
+
version,
|
|
149
|
+
machineReadable: false,
|
|
150
|
+
updateCheck: updateCheckEnabled(),
|
|
151
|
+
});
|
|
96
152
|
});
|
|
97
153
|
program
|
|
98
154
|
.command('doctor')
|
|
99
155
|
.description('Diagnose your OAuthLint install (Node, Semgrep, rule pack)')
|
|
100
156
|
.option('--json', 'Emit JSON instead of pretty output')
|
|
101
157
|
.action(async (opts) => {
|
|
102
|
-
|
|
158
|
+
const code = await runDoctor({ json: opts.json });
|
|
159
|
+
await finishWithNotice(code, {
|
|
160
|
+
version,
|
|
161
|
+
machineReadable: opts.json === true,
|
|
162
|
+
updateCheck: updateCheckEnabled(),
|
|
163
|
+
});
|
|
103
164
|
});
|
|
104
165
|
program.showHelpAfterError(pc.dim('(run `oauthlint --help` for available commands)'));
|
|
105
166
|
return program;
|
|
@@ -116,7 +177,7 @@ function parseFailOn(v) {
|
|
|
116
177
|
return 'off';
|
|
117
178
|
return parseSeverity(v);
|
|
118
179
|
}
|
|
119
|
-
const FORMATS = ['pretty', 'json', 'sarif'];
|
|
180
|
+
const FORMATS = ['pretty', 'json', 'sarif', 'html'];
|
|
120
181
|
function parseFormat(v) {
|
|
121
182
|
const lower = v.toLowerCase();
|
|
122
183
|
if (!FORMATS.includes(lower)) {
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,MAAM,YAAY,CAAC;AAC5B,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACjD,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAC7C,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAC7C,OAAO,EAAmB,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAC9D,OAAO,EAAE,UAAU,EAAqB,MAAM,YAAY,CAAC;AAM3D;;;;;;;;GAQG;AACH,KAAK,UAAU,cAAc,CAAC,IAAY;IACxC,MAAM,KAAK,GAAG,CAAC,CAAqB,EAAiB,EAAE,CACrD,IAAI,OAAO,CAAC,CAAC,GAAG,EAAE,EAAE;QAClB,IAAI,CAAC,CAAC,cAAc,KAAK,CAAC,EAAE,CAAC;YAC3B,GAAG,EAAE,CAAC;YACN,OAAO;QACT,CAAC;QACD,CAAC,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,GAAG,EAAE,CAAC,CAAC;IAC/B,CAAC,CAAC,CAAC;IACL,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;IAClE,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACrB,CAAC;AAED,KAAK,UAAU,kBAAkB;IAC/B,MAAM,IAAI,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;IACrD,iDAAiD;IACjD,8DAA8D;IAC9D,KAAK,MAAM,SAAS,IAAI;QACtB,OAAO,CAAC,IAAI,EAAE,IAAI,EAAE,cAAc,CAAC;QACnC,OAAO,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,cAAc,CAAC;KAC1C,EAAE,CAAC;QACF,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC,CAAY,CAAC;YACrE,IAAI,GAAG,CAAC,OAAO;gBAAE,OAAO,GAAG,CAAC,OAAO,CAAC;QACtC,CAAC;QAAC,MAAM,CAAC;YACP,cAAc;QAChB,CAAC;IACH,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,YAAY;IAChC,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;IAC9B,MAAM,OAAO,GAAG,MAAM,kBAAkB,EAAE,CAAC;IAE3C,OAAO;SACJ,IAAI,CAAC,WAAW,CAAC;SACjB,WAAW,CACV,4FAA4F,CAC7F;SACA,OAAO,CAAC,OAAO,EAAE,eAAe,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { SemgrepAdapter } from '../adapters/semgrep.js';
|
|
2
|
+
export interface BaselineCommandOptions {
|
|
3
|
+
/** Path(s) to scan when capturing the baseline. Defaults to `['.']`. */
|
|
4
|
+
paths?: string[];
|
|
5
|
+
/** Where to write the baseline JSON. Defaults to `.oauthlint-baseline.json`. */
|
|
6
|
+
output?: string;
|
|
7
|
+
rulesDir?: string;
|
|
8
|
+
/** Used by tests to inject a mock adapter. */
|
|
9
|
+
adapter?: SemgrepAdapter;
|
|
10
|
+
/** Used by tests to capture output. */
|
|
11
|
+
stream?: NodeJS.WritableStream;
|
|
12
|
+
/** Override the directory git/relative paths resolve from (defaults to cwd). */
|
|
13
|
+
cwd?: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* `oauthlint baseline [paths...]` — scan the codebase and write a baseline file
|
|
17
|
+
* capturing the CURRENT findings by stable fingerprint, so a later
|
|
18
|
+
* `scan --baseline` only surfaces NEW findings.
|
|
19
|
+
*/
|
|
20
|
+
export declare function runBaseline(opts: BaselineCommandOptions): Promise<number>;
|
|
21
|
+
//# sourceMappingURL=baseline.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"baseline.d.ts","sourceRoot":"","sources":["../../src/commands/baseline.ts"],"names":[],"mappings":"AAIA,OAAO,EACL,cAAc,EAGf,MAAM,wBAAwB,CAAC;AAMhC,MAAM,WAAW,sBAAsB;IACrC,wEAAwE;IACxE,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,gFAAgF;IAChF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,8CAA8C;IAC9C,OAAO,CAAC,EAAE,cAAc,CAAC;IACzB,uCAAuC;IACvC,MAAM,CAAC,EAAE,MAAM,CAAC,cAAc,CAAC;IAC/B,gFAAgF;IAChF,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED;;;;GAIG;AACH,wBAAsB,WAAW,CAAC,IAAI,EAAE,sBAAsB,GAAG,OAAO,CAAC,MAAM,CAAC,CA+C/E"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { writeFile } from 'node:fs/promises';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { RULES_ROOT } from 'oauthlint-rules';
|
|
4
|
+
import pc from 'picocolors';
|
|
5
|
+
import { SemgrepAdapter, SemgrepNotInstalledError, SemgrepOutputError, } from '../adapters/semgrep.js';
|
|
6
|
+
import { DEFAULT_BASELINE_FILE, buildBaseline, serialiseBaseline } from '../core/baseline.js';
|
|
7
|
+
import { loadConfig } from '../core/config.js';
|
|
8
|
+
import { applySuppressions } from '../core/suppress.js';
|
|
9
|
+
/**
|
|
10
|
+
* `oauthlint baseline [paths...]` — scan the codebase and write a baseline file
|
|
11
|
+
* capturing the CURRENT findings by stable fingerprint, so a later
|
|
12
|
+
* `scan --baseline` only surfaces NEW findings.
|
|
13
|
+
*/
|
|
14
|
+
export async function runBaseline(opts) {
|
|
15
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
16
|
+
const config = await loadConfig(cwd);
|
|
17
|
+
const rulesDir = opts.rulesDir ?? config.customRulesDir ?? RULES_ROOT;
|
|
18
|
+
const stream = opts.stream ?? process.stdout;
|
|
19
|
+
const errStream = opts.stream ?? process.stderr;
|
|
20
|
+
const requested = opts.paths?.length ? opts.paths : ['.'];
|
|
21
|
+
const targets = requested.map((p) => resolve(cwd, p));
|
|
22
|
+
const adapter = opts.adapter ?? new SemgrepAdapter({ configPath: rulesDir });
|
|
23
|
+
let result;
|
|
24
|
+
try {
|
|
25
|
+
result = await adapter.scan(targets);
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
if (err instanceof SemgrepNotInstalledError) {
|
|
29
|
+
errStream.write(`${err.message}\n`);
|
|
30
|
+
return 127;
|
|
31
|
+
}
|
|
32
|
+
if (err instanceof SemgrepOutputError) {
|
|
33
|
+
errStream.write(`${err.message}\n`);
|
|
34
|
+
return 2;
|
|
35
|
+
}
|
|
36
|
+
throw err;
|
|
37
|
+
}
|
|
38
|
+
// Honour inline suppression directives so the baseline mirrors what `scan`
|
|
39
|
+
// would actually report (we don't want to baseline already-suppressed noise).
|
|
40
|
+
const { kept } = await applySuppressions(result.findings);
|
|
41
|
+
const findings = kept;
|
|
42
|
+
const outputPath = resolve(cwd, opts.output ?? DEFAULT_BASELINE_FILE);
|
|
43
|
+
const baseline = await buildBaseline(findings, cwd);
|
|
44
|
+
await writeFile(outputPath, serialiseBaseline(baseline), 'utf8');
|
|
45
|
+
const n = baseline.findings.length;
|
|
46
|
+
stream.write(pc.green(`✓ Baselined ${n} finding${n === 1 ? '' : 's'} → ${opts.output ?? DEFAULT_BASELINE_FILE}\n`));
|
|
47
|
+
stream.write(pc.dim(`Future runs of ${pc.bold('oauthlint scan --baseline')} will report only NEW findings.\n`));
|
|
48
|
+
return 0;
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=baseline.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"baseline.js","sourceRoot":"","sources":["../../src/commands/baseline.ts"],"names":[],"mappings":"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"}
|
package/dist/commands/scan.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { SemgrepAdapter } from '../adapters/semgrep.js';
|
|
2
2
|
import type { SeverityName } from '../types.js';
|
|
3
|
-
export type ScanFormat = 'pretty' | 'json' | 'sarif';
|
|
3
|
+
export type ScanFormat = 'pretty' | 'json' | 'sarif' | 'html';
|
|
4
4
|
export interface ScanCommandOptions {
|
|
5
5
|
/**
|
|
6
6
|
* Path(s) to scan. Accepts one or many — e.g. a pre-commit hook or editor
|
|
@@ -30,6 +30,13 @@ export interface ScanCommandOptions {
|
|
|
30
30
|
* ship a `fix:` template. Currently the cookie-* rules.
|
|
31
31
|
*/
|
|
32
32
|
fix?: boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Suppress findings already captured in a baseline file, reporting only NEW
|
|
35
|
+
* findings. A bare `true` uses the default `.oauthlint-baseline.json`; a
|
|
36
|
+
* string is treated as an explicit baseline path. A missing file is a clear
|
|
37
|
+
* error, never silently an empty allow-list.
|
|
38
|
+
*/
|
|
39
|
+
baseline?: string | boolean;
|
|
33
40
|
/** Used by tests to inject a mock adapter. */
|
|
34
41
|
adapter?: SemgrepAdapter;
|
|
35
42
|
/** Used by tests to capture output. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scan.d.ts","sourceRoot":"","sources":["../../src/commands/scan.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,cAAc,EAGf,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"scan.d.ts","sourceRoot":"","sources":["../../src/commands/scan.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,cAAc,EAGf,MAAM,wBAAwB,CAAC;AAehC,OAAO,KAAK,EAAW,YAAY,EAAE,MAAM,aAAa,CAAC;AAEzD,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,CAAC;AAE9D,MAAM,WAAW,kBAAkB;IACjC;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,sEAAsE;IACtE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IACxB,4EAA4E;IAC5E,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,uDAAuD;IACvD,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,gDAAgD;IAChD,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,QAAQ,CAAC,EAAE,YAAY,CAAC;IACxB,MAAM,CAAC,EAAE,YAAY,GAAG,KAAK,CAAC;IAC9B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,GAAG,CAAC,EAAE,OAAO,CAAC;IACd;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IAC5B,8CAA8C;IAC9C,OAAO,CAAC,EAAE,cAAc,CAAC;IACzB,uCAAuC;IACvC,MAAM,CAAC,EAAE,MAAM,CAAC,cAAc,CAAC;IAC/B,gFAAgF;IAChF,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,wBAAsB,OAAO,CAAC,IAAI,EAAE,kBAAkB,GAAG,OAAO,CAAC,MAAM,CAAC,CAkLvE"}
|
package/dist/commands/scan.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { resolve } from 'node:path';
|
|
2
2
|
import { RULES_ROOT } from 'oauthlint-rules';
|
|
3
3
|
import { SemgrepAdapter, SemgrepNotInstalledError, SemgrepOutputError, } from '../adapters/semgrep.js';
|
|
4
|
+
import { BaselineNotFoundError, BaselineParseError, DEFAULT_BASELINE_FILE, loadBaseline, partitionByBaseline, } from '../core/baseline.js';
|
|
4
5
|
import { GitError, resolveDiffFiles, resolveStagedFiles } from '../core/changed-files.js';
|
|
5
6
|
import { loadConfig } from '../core/config.js';
|
|
6
7
|
import { Reporter } from '../core/reporter.js';
|
|
7
8
|
import { toSarif } from '../core/sarif.js';
|
|
8
9
|
import { exitCodeFor, highestSeverity, meetsThreshold } from '../core/severity.js';
|
|
9
10
|
import { applySuppressions } from '../core/suppress.js';
|
|
11
|
+
import { renderHtmlReport } from '../formatters/html.js';
|
|
10
12
|
export async function runScan(opts) {
|
|
11
13
|
const cwd = opts.cwd ?? process.cwd();
|
|
12
14
|
const config = await loadConfig(cwd);
|
|
@@ -15,6 +17,22 @@ export async function runScan(opts) {
|
|
|
15
17
|
const format = opts.format ?? (opts.json ? 'json' : 'pretty');
|
|
16
18
|
const stream = opts.stream ?? process.stdout;
|
|
17
19
|
const errStream = opts.stream ?? process.stderr;
|
|
20
|
+
// Load the baseline up front so a missing/malformed file fails fast — before
|
|
21
|
+
// we pay for a full scan — with a clear error rather than a silent no-op.
|
|
22
|
+
let baseline = null;
|
|
23
|
+
if (opts.baseline !== undefined && opts.baseline !== false) {
|
|
24
|
+
const baselinePath = resolve(cwd, typeof opts.baseline === 'string' ? opts.baseline : DEFAULT_BASELINE_FILE);
|
|
25
|
+
try {
|
|
26
|
+
baseline = await loadBaseline(baselinePath);
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
if (err instanceof BaselineNotFoundError || err instanceof BaselineParseError) {
|
|
30
|
+
errStream.write(`${err.message}\n`);
|
|
31
|
+
return 2;
|
|
32
|
+
}
|
|
33
|
+
throw err;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
18
36
|
// Resolve the set of targets to hand Semgrep. Incremental flags (--diff /
|
|
19
37
|
// --staged) win and narrow the scan to changed files only; otherwise we scan
|
|
20
38
|
// the explicit path args (default ['.']).
|
|
@@ -63,6 +81,11 @@ export async function runScan(opts) {
|
|
|
63
81
|
});
|
|
64
82
|
stream.write(`${JSON.stringify(empty, null, 2)}\n`);
|
|
65
83
|
}
|
|
84
|
+
else if (format === 'html') {
|
|
85
|
+
const html = await renderHtmlReport({ findings: [], scannedFiles: 0, durationMs: 0, semgrepVersion: null, errors: [] }, { target: 'changed files (none)' });
|
|
86
|
+
stream.write(html);
|
|
87
|
+
errStream.write('No changed files to scan.\n');
|
|
88
|
+
}
|
|
66
89
|
else {
|
|
67
90
|
stream.write('No changed files to scan.\n');
|
|
68
91
|
}
|
|
@@ -99,10 +122,31 @@ export async function runScan(opts) {
|
|
|
99
122
|
if (minSeverity) {
|
|
100
123
|
findings = findings.filter((f) => meetsThreshold(f.severity, minSeverity));
|
|
101
124
|
}
|
|
125
|
+
// Baseline suppression: drop findings whose fingerprint is already recorded,
|
|
126
|
+
// so only genuinely NEW findings are reported and gate the exit code.
|
|
127
|
+
let baselinedCount = 0;
|
|
128
|
+
if (baseline) {
|
|
129
|
+
const { newFindings, baselined } = await partitionByBaseline(findings, baseline, cwd);
|
|
130
|
+
findings = newFindings;
|
|
131
|
+
baselinedCount = baselined.length;
|
|
132
|
+
}
|
|
102
133
|
if (format === 'sarif') {
|
|
103
134
|
const sarif = await toSarif({ ...result, findings });
|
|
104
135
|
stream.write(`${JSON.stringify(sarif, null, 2)}\n`);
|
|
105
136
|
}
|
|
137
|
+
else if (format === 'html') {
|
|
138
|
+
// HTML is the report artifact → stdout only. Any human chatter (e.g. the
|
|
139
|
+
// update notice, suppression counts) stays on stderr so the document is
|
|
140
|
+
// never corrupted and `> report.html` is always valid.
|
|
141
|
+
const html = await renderHtmlReport({ ...result, findings }, { target: label });
|
|
142
|
+
stream.write(html);
|
|
143
|
+
if (suppressed.length > 0) {
|
|
144
|
+
errStream.write(`ℹ ${suppressed.length} finding${suppressed.length === 1 ? '' : 's'} suppressed via inline directives.\n`);
|
|
145
|
+
}
|
|
146
|
+
if (baselinedCount > 0) {
|
|
147
|
+
errStream.write(`ℹ ${baselinedCount} finding${baselinedCount === 1 ? '' : 's'} already in the baseline (reporting NEW findings only).\n`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
106
150
|
else {
|
|
107
151
|
reporter.reportResult({ ...result, findings });
|
|
108
152
|
if (opts.fix && format === 'pretty') {
|
|
@@ -112,6 +156,9 @@ export async function runScan(opts) {
|
|
|
112
156
|
if (suppressed.length > 0 && format === 'pretty') {
|
|
113
157
|
stream.write(`\nℹ ${suppressed.length} finding${suppressed.length === 1 ? '' : 's'} suppressed via inline directives.\n`);
|
|
114
158
|
}
|
|
159
|
+
if (baselinedCount > 0 && format === 'pretty') {
|
|
160
|
+
stream.write(`\nℹ ${baselinedCount} finding${baselinedCount === 1 ? '' : 's'} already in the baseline (reporting NEW findings only).\n`);
|
|
161
|
+
}
|
|
115
162
|
}
|
|
116
163
|
if (failOn === 'off')
|
|
117
164
|
return 0;
|