oauthlint 0.2.4 → 0.4.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 +135 -6
- package/dist/adapters/semgrep.d.ts +7 -2
- package/dist/adapters/semgrep.d.ts.map +1 -1
- package/dist/adapters/semgrep.js +12 -3
- package/dist/adapters/semgrep.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +71 -9
- 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/init.d.ts +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +1 -1
- package/dist/commands/scan.d.ts +25 -1
- package/dist/commands/scan.d.ts.map +1 -1
- package/dist/commands/scan.js +88 -4
- 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/changed-files.d.ts +38 -0
- package/dist/core/changed-files.d.ts.map +1 -0
- package/dist/core/changed-files.js +186 -0
- package/dist/core/changed-files.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/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/dist/commands/scan.js
CHANGED
|
@@ -1,25 +1,98 @@
|
|
|
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';
|
|
5
|
+
import { GitError, resolveDiffFiles, resolveStagedFiles } from '../core/changed-files.js';
|
|
4
6
|
import { loadConfig } from '../core/config.js';
|
|
5
7
|
import { Reporter } from '../core/reporter.js';
|
|
6
8
|
import { toSarif } from '../core/sarif.js';
|
|
7
9
|
import { exitCodeFor, highestSeverity, meetsThreshold } from '../core/severity.js';
|
|
8
10
|
import { applySuppressions } from '../core/suppress.js';
|
|
9
11
|
export async function runScan(opts) {
|
|
10
|
-
const
|
|
11
|
-
const config = await loadConfig(
|
|
12
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
13
|
+
const config = await loadConfig(cwd);
|
|
12
14
|
const rulesDir = opts.rulesDir ?? config.customRulesDir ?? RULES_ROOT;
|
|
13
15
|
const failOn = opts.failOn ?? config.failOn ?? 'HIGH';
|
|
14
16
|
const format = opts.format ?? (opts.json ? 'json' : 'pretty');
|
|
15
17
|
const stream = opts.stream ?? process.stdout;
|
|
18
|
+
const errStream = opts.stream ?? process.stderr;
|
|
19
|
+
// Load the baseline up front so a missing/malformed file fails fast — before
|
|
20
|
+
// we pay for a full scan — with a clear error rather than a silent no-op.
|
|
21
|
+
let baseline = null;
|
|
22
|
+
if (opts.baseline !== undefined && opts.baseline !== false) {
|
|
23
|
+
const baselinePath = resolve(cwd, typeof opts.baseline === 'string' ? opts.baseline : DEFAULT_BASELINE_FILE);
|
|
24
|
+
try {
|
|
25
|
+
baseline = await loadBaseline(baselinePath);
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
if (err instanceof BaselineNotFoundError || err instanceof BaselineParseError) {
|
|
29
|
+
errStream.write(`${err.message}\n`);
|
|
30
|
+
return 2;
|
|
31
|
+
}
|
|
32
|
+
throw err;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// Resolve the set of targets to hand Semgrep. Incremental flags (--diff /
|
|
36
|
+
// --staged) win and narrow the scan to changed files only; otherwise we scan
|
|
37
|
+
// the explicit path args (default ['.']).
|
|
38
|
+
let targets;
|
|
39
|
+
let incremental = false;
|
|
40
|
+
try {
|
|
41
|
+
if (opts.diff !== undefined && opts.diff !== false) {
|
|
42
|
+
incremental = true;
|
|
43
|
+
const ref = typeof opts.diff === 'string' ? opts.diff : undefined;
|
|
44
|
+
targets = await resolveDiffFiles(cwd, ref);
|
|
45
|
+
}
|
|
46
|
+
else if (opts.staged) {
|
|
47
|
+
incremental = true;
|
|
48
|
+
targets = await resolveStagedFiles(cwd);
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
const requested = opts.paths?.length
|
|
52
|
+
? opts.paths
|
|
53
|
+
: opts.path !== undefined
|
|
54
|
+
? [opts.path]
|
|
55
|
+
: ['.'];
|
|
56
|
+
targets = requested.map((p) => resolve(cwd, p));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
if (err instanceof GitError) {
|
|
61
|
+
errStream.write(`${err.message}\n`);
|
|
62
|
+
return 2;
|
|
63
|
+
}
|
|
64
|
+
throw err;
|
|
65
|
+
}
|
|
66
|
+
// Incremental change set is empty (nothing in a supported language changed).
|
|
67
|
+
// That's a success, not an error — pre-commit hooks and editors call this
|
|
68
|
+
// constantly and should exit 0 with a clear note rather than failing.
|
|
69
|
+
if (incremental && targets.length === 0) {
|
|
70
|
+
if (format === 'json') {
|
|
71
|
+
stream.write(`${JSON.stringify({ schemaVersion: 'oauthlint-v1', findings: [] })}\n`);
|
|
72
|
+
}
|
|
73
|
+
else if (format === 'sarif') {
|
|
74
|
+
const empty = await toSarif({
|
|
75
|
+
findings: [],
|
|
76
|
+
scannedFiles: 0,
|
|
77
|
+
durationMs: 0,
|
|
78
|
+
semgrepVersion: null,
|
|
79
|
+
errors: [],
|
|
80
|
+
});
|
|
81
|
+
stream.write(`${JSON.stringify(empty, null, 2)}\n`);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
stream.write('No changed files to scan.\n');
|
|
85
|
+
}
|
|
86
|
+
return 0;
|
|
87
|
+
}
|
|
88
|
+
const label = incremental ? `${targets.length} changed file(s)` : targets.join(', ');
|
|
16
89
|
const reporter = new Reporter({ json: format === 'json', stream });
|
|
17
90
|
if (format === 'pretty')
|
|
18
|
-
reporter.reportStart(
|
|
91
|
+
reporter.reportStart(label, await countRuleFiles(rulesDir));
|
|
19
92
|
const adapter = opts.adapter ?? new SemgrepAdapter({ configPath: rulesDir });
|
|
20
93
|
let result;
|
|
21
94
|
try {
|
|
22
|
-
result = await adapter.scan(
|
|
95
|
+
result = await adapter.scan(targets, { applyFixes: opts.fix });
|
|
23
96
|
}
|
|
24
97
|
catch (err) {
|
|
25
98
|
if (err instanceof SemgrepNotInstalledError) {
|
|
@@ -43,6 +116,14 @@ export async function runScan(opts) {
|
|
|
43
116
|
if (minSeverity) {
|
|
44
117
|
findings = findings.filter((f) => meetsThreshold(f.severity, minSeverity));
|
|
45
118
|
}
|
|
119
|
+
// Baseline suppression: drop findings whose fingerprint is already recorded,
|
|
120
|
+
// so only genuinely NEW findings are reported and gate the exit code.
|
|
121
|
+
let baselinedCount = 0;
|
|
122
|
+
if (baseline) {
|
|
123
|
+
const { newFindings, baselined } = await partitionByBaseline(findings, baseline, cwd);
|
|
124
|
+
findings = newFindings;
|
|
125
|
+
baselinedCount = baselined.length;
|
|
126
|
+
}
|
|
46
127
|
if (format === 'sarif') {
|
|
47
128
|
const sarif = await toSarif({ ...result, findings });
|
|
48
129
|
stream.write(`${JSON.stringify(sarif, null, 2)}\n`);
|
|
@@ -56,6 +137,9 @@ export async function runScan(opts) {
|
|
|
56
137
|
if (suppressed.length > 0 && format === 'pretty') {
|
|
57
138
|
stream.write(`\nℹ ${suppressed.length} finding${suppressed.length === 1 ? '' : 's'} suppressed via inline directives.\n`);
|
|
58
139
|
}
|
|
140
|
+
if (baselinedCount > 0 && format === 'pretty') {
|
|
141
|
+
stream.write(`\nℹ ${baselinedCount} finding${baselinedCount === 1 ? '' : 's'} already in the baseline (reporting NEW findings only).\n`);
|
|
142
|
+
}
|
|
59
143
|
}
|
|
60
144
|
if (failOn === 'off')
|
|
61
145
|
return 0;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scan.js","sourceRoot":"","sources":["../../src/commands/scan.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,EACL,cAAc,EACd,wBAAwB,EACxB,kBAAkB,GACnB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAC/C,OAAO,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAC3C,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACnF,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"scan.js","sourceRoot":"","sources":["../../src/commands/scan.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { Finding } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Baseline support: capture the CURRENT set of findings so a team can adopt
|
|
4
|
+
* OAuthLint on a large existing codebase and only be alerted on NEW issues.
|
|
5
|
+
*
|
|
6
|
+
* The hard requirement is a fingerprint that is stable across edits that move
|
|
7
|
+
* a finding around without changing it: inserting an import, reformatting a
|
|
8
|
+
* block, or shifting code down N lines must NOT resurface a baselined finding,
|
|
9
|
+
* while genuinely changing the flagged code MUST surface it as new.
|
|
10
|
+
*
|
|
11
|
+
* Fingerprint = sha256 of three parts joined by NULs:
|
|
12
|
+
* 1. the rule identity — `oauthlintRuleId` when present, else `ruleId`
|
|
13
|
+
* 2. the repo/cwd-relative file path (POSIX separators, so it's portable)
|
|
14
|
+
* 3. a NORMALISED snapshot of the matched source (the `startLine..endLine`
|
|
15
|
+
* span, whitespace-collapsed) — explicitly NOT the raw line numbers
|
|
16
|
+
*
|
|
17
|
+
* When the file can't be read (deleted, binary, permission error) we fall back
|
|
18
|
+
* to the trimmed `message` as the snapshot so a fingerprint can still be
|
|
19
|
+
* produced — degraded but deterministic, never a crash.
|
|
20
|
+
*
|
|
21
|
+
* Identical fingerprints within one file (e.g. the same anti-pattern repeated)
|
|
22
|
+
* are disambiguated by a stable per-file occurrence index, assigned in source
|
|
23
|
+
* order, so each physical finding maps to a distinct baseline entry.
|
|
24
|
+
*/
|
|
25
|
+
export declare const BASELINE_VERSION: 1;
|
|
26
|
+
export declare const DEFAULT_BASELINE_FILE = ".oauthlint-baseline.json";
|
|
27
|
+
/** A single baselined finding, keyed by its stable fingerprint. */
|
|
28
|
+
export interface BaselineEntry {
|
|
29
|
+
/** Stable fingerprint (see module docs). Survives line shifts. */
|
|
30
|
+
fingerprint: string;
|
|
31
|
+
/** Rule identity used in the hash — for human-diffability of the file. */
|
|
32
|
+
ruleId: string;
|
|
33
|
+
/** Repo/cwd-relative path used in the hash — for human-diffability. */
|
|
34
|
+
filePath: string;
|
|
35
|
+
}
|
|
36
|
+
export interface BaselineFile {
|
|
37
|
+
version: typeof BASELINE_VERSION;
|
|
38
|
+
generatedAt: string;
|
|
39
|
+
/** Sorted, deterministic list of baselined fingerprints. */
|
|
40
|
+
findings: BaselineEntry[];
|
|
41
|
+
}
|
|
42
|
+
/** In-memory baseline: just the set of fingerprints we suppress against. */
|
|
43
|
+
export interface Baseline {
|
|
44
|
+
fingerprints: Set<string>;
|
|
45
|
+
}
|
|
46
|
+
export declare class BaselineNotFoundError extends Error {
|
|
47
|
+
constructor(path: string);
|
|
48
|
+
}
|
|
49
|
+
export declare class BaselineParseError extends Error {
|
|
50
|
+
constructor(path: string, detail: string);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Turn a finding's `filePath` into a portable, repo/cwd-relative path with
|
|
54
|
+
* POSIX separators. Absolute paths are made relative to `cwd`; paths that are
|
|
55
|
+
* already relative are normalised as-is. We never let an absolute path leak
|
|
56
|
+
* into a fingerprint, so a baseline is portable across machines/checkouts.
|
|
57
|
+
*/
|
|
58
|
+
export declare function toRelativePath(filePath: string, cwd: string): string;
|
|
59
|
+
/**
|
|
60
|
+
* Compute the stable fingerprint for a finding (without the occurrence index).
|
|
61
|
+
* Two findings of the same rule on the same (relative) file with the same
|
|
62
|
+
* normalised snippet share this value — they're disambiguated by the index
|
|
63
|
+
* added in {@link fingerprintFindings}.
|
|
64
|
+
*/
|
|
65
|
+
export declare function baseFingerprint(finding: Finding, cwd: string, cache: Map<string, string[] | null>): Promise<string>;
|
|
66
|
+
/** A finding paired with its final, occurrence-disambiguated fingerprint. */
|
|
67
|
+
export interface FingerprintedFinding {
|
|
68
|
+
finding: Finding;
|
|
69
|
+
fingerprint: string;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Fingerprint a list of findings deterministically. Findings are processed in
|
|
73
|
+
* a stable order, and identical base fingerprints within the run get an
|
|
74
|
+
* occurrence index (`<base>:<n>`) so repeated anti-patterns each get a unique
|
|
75
|
+
* key. The same input list always yields the same fingerprints.
|
|
76
|
+
*/
|
|
77
|
+
export declare function fingerprintFindings(findings: Finding[], cwd: string): Promise<FingerprintedFinding[]>;
|
|
78
|
+
/**
|
|
79
|
+
* Build a serialisable baseline file from a set of findings. The entry list is
|
|
80
|
+
* sorted so the JSON is stable and human-diffable across runs.
|
|
81
|
+
*/
|
|
82
|
+
export declare function buildBaseline(findings: Finding[], cwd: string): Promise<BaselineFile>;
|
|
83
|
+
/** Serialise a baseline file to pretty JSON (trailing newline for clean diffs). */
|
|
84
|
+
export declare function serialiseBaseline(baseline: BaselineFile): string;
|
|
85
|
+
/**
|
|
86
|
+
* Load a baseline from disk into an in-memory fingerprint set.
|
|
87
|
+
*
|
|
88
|
+
* @throws BaselineNotFoundError when the file is absent — deliberately NOT
|
|
89
|
+
* treated as an empty allow-list, so a misspelt `--baseline path` fails loud
|
|
90
|
+
* instead of silently suppressing nothing (or everything).
|
|
91
|
+
* @throws BaselineParseError when the file is present but malformed.
|
|
92
|
+
*/
|
|
93
|
+
export declare function loadBaseline(path: string): Promise<Baseline>;
|
|
94
|
+
/**
|
|
95
|
+
* Split findings into those already present in the baseline (suppressed) and
|
|
96
|
+
* genuinely new ones, by fingerprinting and matching against the baseline set.
|
|
97
|
+
*/
|
|
98
|
+
export declare function partitionByBaseline(findings: Finding[], baseline: Baseline, cwd: string): Promise<{
|
|
99
|
+
newFindings: Finding[];
|
|
100
|
+
baselined: Finding[];
|
|
101
|
+
}>;
|
|
102
|
+
//# sourceMappingURL=baseline.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"baseline.d.ts","sourceRoot":"","sources":["../../src/core/baseline.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAE3C;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,eAAO,MAAM,gBAAgB,EAAG,CAAU,CAAC;AAC3C,eAAO,MAAM,qBAAqB,6BAA6B,CAAC;AAEhE,mEAAmE;AACnE,MAAM,WAAW,aAAa;IAC5B,kEAAkE;IAClE,WAAW,EAAE,MAAM,CAAC;IACpB,0EAA0E;IAC1E,MAAM,EAAE,MAAM,CAAC;IACf,uEAAuE;IACvE,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,OAAO,gBAAgB,CAAC;IACjC,WAAW,EAAE,MAAM,CAAC;IACpB,4DAA4D;IAC5D,QAAQ,EAAE,aAAa,EAAE,CAAC;CAC3B;AAED,4EAA4E;AAC5E,MAAM,WAAW,QAAQ;IACvB,YAAY,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CAC3B;AAED,qBAAa,qBAAsB,SAAQ,KAAK;gBAClC,IAAI,EAAE,MAAM;CAMzB;AAED,qBAAa,kBAAmB,SAAQ,KAAK;gBAC/B,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;CAIzC;AAYD;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAGpE;AAuDD;;;;;GAKG;AACH,wBAAsB,eAAe,CACnC,OAAO,EAAE,OAAO,EAChB,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC,GAClC,OAAO,CAAC,MAAM,CAAC,CAIjB;AAED,6EAA6E;AAC7E,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,OAAO,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;;;;GAKG;AACH,wBAAsB,mBAAmB,CACvC,QAAQ,EAAE,OAAO,EAAE,EACnB,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,oBAAoB,EAAE,CAAC,CAuBjC;AAED;;;GAGG;AACH,wBAAsB,aAAa,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAkB3F;AAED,mFAAmF;AACnF,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,YAAY,GAAG,MAAM,CAEhE;AAED;;;;;;;GAOG;AACH,wBAAsB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,CA8BlE;AAED;;;GAGG;AACH,wBAAsB,mBAAmB,CACvC,QAAQ,EAAE,OAAO,EAAE,EACnB,QAAQ,EAAE,QAAQ,EAClB,GAAG,EAAE,MAAM,GACV,OAAO,CAAC;IAAE,WAAW,EAAE,OAAO,EAAE,CAAC;IAAC,SAAS,EAAE,OAAO,EAAE,CAAA;CAAE,CAAC,CAS3D"}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { isAbsolute, relative, resolve } from 'node:path';
|
|
4
|
+
/**
|
|
5
|
+
* Baseline support: capture the CURRENT set of findings so a team can adopt
|
|
6
|
+
* OAuthLint on a large existing codebase and only be alerted on NEW issues.
|
|
7
|
+
*
|
|
8
|
+
* The hard requirement is a fingerprint that is stable across edits that move
|
|
9
|
+
* a finding around without changing it: inserting an import, reformatting a
|
|
10
|
+
* block, or shifting code down N lines must NOT resurface a baselined finding,
|
|
11
|
+
* while genuinely changing the flagged code MUST surface it as new.
|
|
12
|
+
*
|
|
13
|
+
* Fingerprint = sha256 of three parts joined by NULs:
|
|
14
|
+
* 1. the rule identity — `oauthlintRuleId` when present, else `ruleId`
|
|
15
|
+
* 2. the repo/cwd-relative file path (POSIX separators, so it's portable)
|
|
16
|
+
* 3. a NORMALISED snapshot of the matched source (the `startLine..endLine`
|
|
17
|
+
* span, whitespace-collapsed) — explicitly NOT the raw line numbers
|
|
18
|
+
*
|
|
19
|
+
* When the file can't be read (deleted, binary, permission error) we fall back
|
|
20
|
+
* to the trimmed `message` as the snapshot so a fingerprint can still be
|
|
21
|
+
* produced — degraded but deterministic, never a crash.
|
|
22
|
+
*
|
|
23
|
+
* Identical fingerprints within one file (e.g. the same anti-pattern repeated)
|
|
24
|
+
* are disambiguated by a stable per-file occurrence index, assigned in source
|
|
25
|
+
* order, so each physical finding maps to a distinct baseline entry.
|
|
26
|
+
*/
|
|
27
|
+
export const BASELINE_VERSION = 1;
|
|
28
|
+
export const DEFAULT_BASELINE_FILE = '.oauthlint-baseline.json';
|
|
29
|
+
export class BaselineNotFoundError extends Error {
|
|
30
|
+
constructor(path) {
|
|
31
|
+
super(`Baseline file not found: ${path}\nCreate one first with: oauthlint baseline\n(an absent baseline is treated as an error, not an empty allow-list, so a typo never silently disables suppression).`);
|
|
32
|
+
this.name = 'BaselineNotFoundError';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export class BaselineParseError extends Error {
|
|
36
|
+
constructor(path, detail) {
|
|
37
|
+
super(`Could not parse baseline file ${path}: ${detail}`);
|
|
38
|
+
this.name = 'BaselineParseError';
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/** Collapse runs of whitespace and trim, so reformatting doesn't churn fingerprints. */
|
|
42
|
+
function normaliseSnippet(text) {
|
|
43
|
+
return text.replace(/\s+/g, ' ').trim();
|
|
44
|
+
}
|
|
45
|
+
/** Rule identity that goes into the hash — prefer the stable OAuthLint id. */
|
|
46
|
+
function ruleIdentity(finding) {
|
|
47
|
+
return finding.oauthlintRuleId ?? finding.ruleId;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Turn a finding's `filePath` into a portable, repo/cwd-relative path with
|
|
51
|
+
* POSIX separators. Absolute paths are made relative to `cwd`; paths that are
|
|
52
|
+
* already relative are normalised as-is. We never let an absolute path leak
|
|
53
|
+
* into a fingerprint, so a baseline is portable across machines/checkouts.
|
|
54
|
+
*/
|
|
55
|
+
export function toRelativePath(filePath, cwd) {
|
|
56
|
+
const rel = isAbsolute(filePath) ? relative(cwd, filePath) : filePath;
|
|
57
|
+
return rel.split('\\').join('/');
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Read the matched source span for a finding and return it normalised. On any
|
|
61
|
+
* read/decoding problem, fall back to the finding's message so we still get a
|
|
62
|
+
* deterministic value rather than throwing.
|
|
63
|
+
*/
|
|
64
|
+
async function snapshotFor(finding, cwd, cache) {
|
|
65
|
+
const abs = isAbsolute(finding.filePath) ? finding.filePath : resolve(cwd, finding.filePath);
|
|
66
|
+
let lines = cache.get(abs);
|
|
67
|
+
if (lines === undefined) {
|
|
68
|
+
lines = await readLinesSafe(abs);
|
|
69
|
+
cache.set(abs, lines);
|
|
70
|
+
}
|
|
71
|
+
if (lines === null) {
|
|
72
|
+
// Unreadable (missing/binary/permission) — degrade to the message text.
|
|
73
|
+
return normaliseSnippet(finding.message);
|
|
74
|
+
}
|
|
75
|
+
// Finding lines are 1-based and inclusive. Clamp into range defensively in
|
|
76
|
+
// case the file changed between scan and fingerprinting.
|
|
77
|
+
const start = Math.max(1, finding.startLine);
|
|
78
|
+
const end = Math.max(start, finding.endLine);
|
|
79
|
+
const span = lines.slice(start - 1, end).join('\n');
|
|
80
|
+
const normalised = normaliseSnippet(span);
|
|
81
|
+
// If the span is empty (out-of-range lines after a shrink), fall back so the
|
|
82
|
+
// fingerprint is never the hash of an empty string for every such finding.
|
|
83
|
+
return normalised || normaliseSnippet(finding.message);
|
|
84
|
+
}
|
|
85
|
+
/** Read a file as lines, returning null when it's missing or looks binary. */
|
|
86
|
+
async function readLinesSafe(absPath) {
|
|
87
|
+
let buf;
|
|
88
|
+
try {
|
|
89
|
+
buf = await readFile(absPath);
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
// Treat NUL bytes as the binary signal (cheap, matches common heuristics).
|
|
95
|
+
if (buf.includes(0))
|
|
96
|
+
return null;
|
|
97
|
+
return buf.toString('utf8').split('\n');
|
|
98
|
+
}
|
|
99
|
+
function hash(parts) {
|
|
100
|
+
const h = createHash('sha256');
|
|
101
|
+
// NUL separator can't appear in any of the parts (path/id are text, the
|
|
102
|
+
// snippet has NULs stripped by the binary check), so there's no collision
|
|
103
|
+
// between e.g. ["ab","c"] and ["a","bc"].
|
|
104
|
+
h.update(parts.join('\0'));
|
|
105
|
+
return h.digest('hex');
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Compute the stable fingerprint for a finding (without the occurrence index).
|
|
109
|
+
* Two findings of the same rule on the same (relative) file with the same
|
|
110
|
+
* normalised snippet share this value — they're disambiguated by the index
|
|
111
|
+
* added in {@link fingerprintFindings}.
|
|
112
|
+
*/
|
|
113
|
+
export async function baseFingerprint(finding, cwd, cache) {
|
|
114
|
+
const rel = toRelativePath(finding.filePath, cwd);
|
|
115
|
+
const snippet = await snapshotFor(finding, cwd, cache);
|
|
116
|
+
return hash([ruleIdentity(finding), rel, snippet]);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Fingerprint a list of findings deterministically. Findings are processed in
|
|
120
|
+
* a stable order, and identical base fingerprints within the run get an
|
|
121
|
+
* occurrence index (`<base>:<n>`) so repeated anti-patterns each get a unique
|
|
122
|
+
* key. The same input list always yields the same fingerprints.
|
|
123
|
+
*/
|
|
124
|
+
export async function fingerprintFindings(findings, cwd) {
|
|
125
|
+
const cache = new Map();
|
|
126
|
+
// Stable processing order so occurrence indices are reproducible regardless
|
|
127
|
+
// of the adapter's emission order: by relative path, then line, then rule.
|
|
128
|
+
const ordered = [...findings]
|
|
129
|
+
.map((finding) => ({ finding, rel: toRelativePath(finding.filePath, cwd) }))
|
|
130
|
+
.sort((a, b) => a.rel.localeCompare(b.rel) ||
|
|
131
|
+
a.finding.startLine - b.finding.startLine ||
|
|
132
|
+
a.finding.endLine - b.finding.endLine ||
|
|
133
|
+
ruleIdentity(a.finding).localeCompare(ruleIdentity(b.finding)));
|
|
134
|
+
const seen = new Map();
|
|
135
|
+
const out = [];
|
|
136
|
+
for (const { finding } of ordered) {
|
|
137
|
+
const base = await baseFingerprint(finding, cwd, cache);
|
|
138
|
+
const n = seen.get(base) ?? 0;
|
|
139
|
+
seen.set(base, n + 1);
|
|
140
|
+
out.push({ finding, fingerprint: `${base}:${n}` });
|
|
141
|
+
}
|
|
142
|
+
return out;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Build a serialisable baseline file from a set of findings. The entry list is
|
|
146
|
+
* sorted so the JSON is stable and human-diffable across runs.
|
|
147
|
+
*/
|
|
148
|
+
export async function buildBaseline(findings, cwd) {
|
|
149
|
+
const fingerprinted = await fingerprintFindings(findings, cwd);
|
|
150
|
+
const entries = fingerprinted.map(({ finding, fingerprint }) => ({
|
|
151
|
+
fingerprint,
|
|
152
|
+
ruleId: ruleIdentity(finding),
|
|
153
|
+
filePath: toRelativePath(finding.filePath, cwd),
|
|
154
|
+
}));
|
|
155
|
+
entries.sort((a, b) => a.filePath.localeCompare(b.filePath) ||
|
|
156
|
+
a.ruleId.localeCompare(b.ruleId) ||
|
|
157
|
+
a.fingerprint.localeCompare(b.fingerprint));
|
|
158
|
+
return {
|
|
159
|
+
version: BASELINE_VERSION,
|
|
160
|
+
generatedAt: new Date().toISOString(),
|
|
161
|
+
findings: entries,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
/** Serialise a baseline file to pretty JSON (trailing newline for clean diffs). */
|
|
165
|
+
export function serialiseBaseline(baseline) {
|
|
166
|
+
return `${JSON.stringify(baseline, null, 2)}\n`;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Load a baseline from disk into an in-memory fingerprint set.
|
|
170
|
+
*
|
|
171
|
+
* @throws BaselineNotFoundError when the file is absent — deliberately NOT
|
|
172
|
+
* treated as an empty allow-list, so a misspelt `--baseline path` fails loud
|
|
173
|
+
* instead of silently suppressing nothing (or everything).
|
|
174
|
+
* @throws BaselineParseError when the file is present but malformed.
|
|
175
|
+
*/
|
|
176
|
+
export async function loadBaseline(path) {
|
|
177
|
+
let raw;
|
|
178
|
+
try {
|
|
179
|
+
raw = await readFile(path, 'utf8');
|
|
180
|
+
}
|
|
181
|
+
catch (err) {
|
|
182
|
+
if (err.code === 'ENOENT') {
|
|
183
|
+
throw new BaselineNotFoundError(path);
|
|
184
|
+
}
|
|
185
|
+
throw err;
|
|
186
|
+
}
|
|
187
|
+
let parsed;
|
|
188
|
+
try {
|
|
189
|
+
parsed = JSON.parse(raw);
|
|
190
|
+
}
|
|
191
|
+
catch (err) {
|
|
192
|
+
throw new BaselineParseError(path, err instanceof Error ? err.message : String(err));
|
|
193
|
+
}
|
|
194
|
+
if (typeof parsed !== 'object' ||
|
|
195
|
+
parsed === null ||
|
|
196
|
+
!Array.isArray(parsed.findings)) {
|
|
197
|
+
throw new BaselineParseError(path, 'expected an object with a "findings" array');
|
|
198
|
+
}
|
|
199
|
+
const fingerprints = new Set();
|
|
200
|
+
for (const entry of parsed.findings) {
|
|
201
|
+
if (entry && typeof entry.fingerprint === 'string') {
|
|
202
|
+
fingerprints.add(entry.fingerprint);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return { fingerprints };
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Split findings into those already present in the baseline (suppressed) and
|
|
209
|
+
* genuinely new ones, by fingerprinting and matching against the baseline set.
|
|
210
|
+
*/
|
|
211
|
+
export async function partitionByBaseline(findings, baseline, cwd) {
|
|
212
|
+
const fingerprinted = await fingerprintFindings(findings, cwd);
|
|
213
|
+
const newFindings = [];
|
|
214
|
+
const baselined = [];
|
|
215
|
+
for (const { finding, fingerprint } of fingerprinted) {
|
|
216
|
+
if (baseline.fingerprints.has(fingerprint))
|
|
217
|
+
baselined.push(finding);
|
|
218
|
+
else
|
|
219
|
+
newFindings.push(finding);
|
|
220
|
+
}
|
|
221
|
+
return { newFindings, baselined };
|
|
222
|
+
}
|
|
223
|
+
//# sourceMappingURL=baseline.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"baseline.js","sourceRoot":"","sources":["../../src/core/baseline.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Raised when a git operation can't be performed — most commonly because the
|
|
3
|
+
* target directory is not inside a git work tree, or `git` itself is missing.
|
|
4
|
+
* Callers (the scan command) translate this into a clean, non-crashing error
|
|
5
|
+
* message rather than a stack trace.
|
|
6
|
+
*/
|
|
7
|
+
export declare class GitError extends Error {
|
|
8
|
+
constructor(message: string);
|
|
9
|
+
}
|
|
10
|
+
/** True when `file` has an extension the rule pack can actually scan. */
|
|
11
|
+
export declare function isScannableSourceFile(file: string): boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Files changed versus `ref` (or the default branch's merge-base when `ref` is
|
|
14
|
+
* undefined), as absolute paths, filtered to scannable source files.
|
|
15
|
+
*
|
|
16
|
+
* Combines four sets so nothing in flight is missed:
|
|
17
|
+
* - committed changes since `ref` (`git diff --name-only <ref>...`)
|
|
18
|
+
* - unstaged work-tree changes (`git diff --name-only`)
|
|
19
|
+
* - staged-but-uncommitted changes (`git diff --name-only --cached`)
|
|
20
|
+
* - new, not-yet-tracked files (`git ls-files --others --exclude-standard`)
|
|
21
|
+
*
|
|
22
|
+
* `--diff-filter=ACMR` keeps Added / Copied / Modified / Renamed and drops
|
|
23
|
+
* Deleted files (we can't scan a file that no longer exists). Untracked files
|
|
24
|
+
* are picked up separately because plain `git diff` only reports tracked paths —
|
|
25
|
+
* a freshly written, un-`add`ed source file would otherwise be missed.
|
|
26
|
+
*
|
|
27
|
+
* @throws GitError when not in a git repo (or git is unavailable).
|
|
28
|
+
*/
|
|
29
|
+
export declare function resolveDiffFiles(cwd: string, ref?: string): Promise<string[]>;
|
|
30
|
+
/**
|
|
31
|
+
* Staged files (`git diff --name-only --cached --diff-filter=ACMR`) as absolute
|
|
32
|
+
* paths, filtered to scannable source files. This is the set a pre-commit hook
|
|
33
|
+
* cares about.
|
|
34
|
+
*
|
|
35
|
+
* @throws GitError when not in a git repo (or git is unavailable).
|
|
36
|
+
*/
|
|
37
|
+
export declare function resolveStagedFiles(cwd: string): Promise<string[]>;
|
|
38
|
+
//# sourceMappingURL=changed-files.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"changed-files.d.ts","sourceRoot":"","sources":["../../src/core/changed-files.ts"],"names":[],"mappings":"AAGA;;;;;GAKG;AACH,qBAAa,QAAS,SAAQ,KAAK;gBACrB,OAAO,EAAE,MAAM;CAI5B;AAiCD,yEAAyE;AACzE,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAE3D;AA0FD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAmBnF;AAED;;;;;;GAMG;AACH,wBAAsB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAMvE"}
|