@comity-dev/validate 0.2.0 → 0.2.1
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/dist/bin/comity-validate.js +5 -2
- package/dist/engines/semgrep.d.ts +24 -2
- package/dist/engines/semgrep.js +99 -16
- package/dist/format.js +14 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/run.d.ts +10 -0
- package/dist/run.js +15 -0
- package/package.json +2 -2
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
import { resolve } from "node:path";
|
|
27
27
|
import { ExitCode } from "../exit-codes.js";
|
|
28
28
|
import { formatSummary, formatViolations } from "../format.js";
|
|
29
|
-
import { runValidation } from "../run.js";
|
|
29
|
+
import { exitCodeForResult, runValidation } from "../run.js";
|
|
30
30
|
function parseArgs(argv) {
|
|
31
31
|
const args = {
|
|
32
32
|
root: process.cwd(),
|
|
@@ -88,7 +88,10 @@ async function main() {
|
|
|
88
88
|
console.log("");
|
|
89
89
|
console.log(formatViolations(result.violations));
|
|
90
90
|
}
|
|
91
|
-
|
|
91
|
+
// A required tool that could not run must never surface as PASS:
|
|
92
|
+
// report it with the documented TOOL_MISSING exit code instead of
|
|
93
|
+
// fabricating a passing result.
|
|
94
|
+
process.exit(exitCodeForResult(result));
|
|
92
95
|
}
|
|
93
96
|
catch (err) {
|
|
94
97
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -1,18 +1,40 @@
|
|
|
1
1
|
import type { Repository } from "../discover.js";
|
|
2
2
|
import type { EngineResult } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Derive the Python interpreter from a semgrep executable's shebang
|
|
5
|
+
* line. Shim installs (Homebrew, pipx, venv) point `#!` at the
|
|
6
|
+
* interpreter that ships the `semgrep` module; resolving it keeps the
|
|
7
|
+
* scan on a working interpreter instead of an unrelated system python
|
|
8
|
+
* that merely exists on PATH.
|
|
9
|
+
*
|
|
10
|
+
* @param binPath - Path to the semgrep executable.
|
|
11
|
+
* @param env - Environment to read for `/usr/bin/env` resolution.
|
|
12
|
+
*
|
|
13
|
+
* @returns The interpreter path when the file starts with a usable
|
|
14
|
+
* `#!` line naming an existing executable, otherwise `null`.
|
|
15
|
+
*/
|
|
16
|
+
export declare function resolveInterpreterFromBin(binPath: string, env?: NodeJS.ProcessEnv): string | null;
|
|
3
17
|
/**
|
|
4
18
|
* Discover the Python interpreter that ships the `semgrep` module.
|
|
5
19
|
*
|
|
6
20
|
* Selection order:
|
|
7
21
|
* 1. `SEMGREP_PYTHON` when set to an existing path — explicit operator
|
|
8
22
|
* configuration always wins and is never silently overridden.
|
|
9
|
-
* 2.
|
|
23
|
+
* 2. The interpreter named by the semgrep executable's shebang line
|
|
24
|
+
* (shim installs point at the interpreter that ships the module).
|
|
25
|
+
* 3. Well-known absolute paths, then `python3` on PATH (historic default).
|
|
26
|
+
*
|
|
27
|
+
* Every auto-discovered candidate (steps 2-3) must both exist and
|
|
28
|
+
* successfully `import semgrep`; the first candidate that does wins.
|
|
29
|
+
* An explicit `SEMGREP_PYTHON` is honored as-is for backwards
|
|
30
|
+
* compatibility.
|
|
10
31
|
*
|
|
11
32
|
* @param env - Environment to read. Defaults to the process environment.
|
|
33
|
+
* @param binPath - Resolved semgrep executable used for step 2.
|
|
12
34
|
*
|
|
13
35
|
* @returns The selected interpreter path, or `null` when none is available.
|
|
14
36
|
* A set-but-missing `SEMGREP_PYTHON` also yields `null` so the caller can
|
|
15
37
|
* report it as invalid configuration rather than falling back silently.
|
|
16
38
|
*/
|
|
17
|
-
export declare function resolvePythonForSemgrep(env?: NodeJS.ProcessEnv): string | null;
|
|
39
|
+
export declare function resolvePythonForSemgrep(env?: NodeJS.ProcessEnv, binPath?: string): string | null;
|
|
18
40
|
export declare function runSemgrep(repo: Repository): Promise<EngineResult>;
|
package/dist/engines/semgrep.js
CHANGED
|
@@ -1,41 +1,121 @@
|
|
|
1
1
|
import { buildSemgrepConfig } from "@comity-dev/semgrep-rules";
|
|
2
2
|
import { spawnSync } from "node:child_process";
|
|
3
|
-
import { existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { closeSync, existsSync, mkdtempSync, openSync, readSync, rmSync, writeFileSync, } from "node:fs";
|
|
4
4
|
import { relative, resolve } from "node:path";
|
|
5
|
+
/**
|
|
6
|
+
* Derive the Python interpreter from a semgrep executable's shebang
|
|
7
|
+
* line. Shim installs (Homebrew, pipx, venv) point `#!` at the
|
|
8
|
+
* interpreter that ships the `semgrep` module; resolving it keeps the
|
|
9
|
+
* scan on a working interpreter instead of an unrelated system python
|
|
10
|
+
* that merely exists on PATH.
|
|
11
|
+
*
|
|
12
|
+
* @param binPath - Path to the semgrep executable.
|
|
13
|
+
* @param env - Environment to read for `/usr/bin/env` resolution.
|
|
14
|
+
*
|
|
15
|
+
* @returns The interpreter path when the file starts with a usable
|
|
16
|
+
* `#!` line naming an existing executable, otherwise `null`.
|
|
17
|
+
*/
|
|
18
|
+
export function resolveInterpreterFromBin(binPath, env = process.env) {
|
|
19
|
+
let head;
|
|
20
|
+
try {
|
|
21
|
+
const fd = openSync(binPath, "r");
|
|
22
|
+
try {
|
|
23
|
+
const buf = Buffer.alloc(2048);
|
|
24
|
+
const read = readSync(fd, buf, 0, buf.length, 0);
|
|
25
|
+
head = buf.subarray(0, read).toString("utf8").split("\n")[0] ?? "";
|
|
26
|
+
}
|
|
27
|
+
finally {
|
|
28
|
+
closeSync(fd);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
if (!head.startsWith("#!"))
|
|
35
|
+
return null;
|
|
36
|
+
const parts = head.slice(2).trim().split(/\s+/).filter(Boolean);
|
|
37
|
+
const [first, second] = parts;
|
|
38
|
+
if (!first)
|
|
39
|
+
return null;
|
|
40
|
+
// `#!/usr/bin/env python3 ...` form: resolve the named interpreter
|
|
41
|
+
// against PATH the same way a shell would.
|
|
42
|
+
if (first === "env" || first.endsWith("/env")) {
|
|
43
|
+
if (!second)
|
|
44
|
+
return null;
|
|
45
|
+
const probe = spawnSync("which", [second], {
|
|
46
|
+
encoding: "utf8",
|
|
47
|
+
env: { ...env, PATH: env["PATH"] ?? "" },
|
|
48
|
+
});
|
|
49
|
+
if (probe.status === 0 && probe.stdout?.trim()) {
|
|
50
|
+
return probe.stdout.trim();
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
return existsSync(first) ? first : null;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Probe whether an interpreter ships the `semgrep` module, so a
|
|
58
|
+
* resolved python is never assumed to work by mere existence.
|
|
59
|
+
*/
|
|
60
|
+
function interpreterShipsSemgrep(interpreter) {
|
|
61
|
+
try {
|
|
62
|
+
const probe = spawnSync(interpreter, ["-c", "import semgrep"], {
|
|
63
|
+
encoding: "utf8",
|
|
64
|
+
timeout: 30000,
|
|
65
|
+
});
|
|
66
|
+
return probe.status === 0;
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
5
72
|
/**
|
|
6
73
|
* Discover the Python interpreter that ships the `semgrep` module.
|
|
7
74
|
*
|
|
8
75
|
* Selection order:
|
|
9
76
|
* 1. `SEMGREP_PYTHON` when set to an existing path — explicit operator
|
|
10
77
|
* configuration always wins and is never silently overridden.
|
|
11
|
-
* 2.
|
|
78
|
+
* 2. The interpreter named by the semgrep executable's shebang line
|
|
79
|
+
* (shim installs point at the interpreter that ships the module).
|
|
80
|
+
* 3. Well-known absolute paths, then `python3` on PATH (historic default).
|
|
81
|
+
*
|
|
82
|
+
* Every auto-discovered candidate (steps 2-3) must both exist and
|
|
83
|
+
* successfully `import semgrep`; the first candidate that does wins.
|
|
84
|
+
* An explicit `SEMGREP_PYTHON` is honored as-is for backwards
|
|
85
|
+
* compatibility.
|
|
12
86
|
*
|
|
13
87
|
* @param env - Environment to read. Defaults to the process environment.
|
|
88
|
+
* @param binPath - Resolved semgrep executable used for step 2.
|
|
14
89
|
*
|
|
15
90
|
* @returns The selected interpreter path, or `null` when none is available.
|
|
16
91
|
* A set-but-missing `SEMGREP_PYTHON` also yields `null` so the caller can
|
|
17
92
|
* report it as invalid configuration rather than falling back silently.
|
|
18
93
|
*/
|
|
19
|
-
export function resolvePythonForSemgrep(env = process.env) {
|
|
94
|
+
export function resolvePythonForSemgrep(env = process.env, binPath) {
|
|
20
95
|
const explicit = env["SEMGREP_PYTHON"];
|
|
21
96
|
if (explicit) {
|
|
22
97
|
return existsSync(explicit) ? explicit : null;
|
|
23
98
|
}
|
|
24
|
-
const candidates = [
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
99
|
+
const candidates = [];
|
|
100
|
+
if (binPath) {
|
|
101
|
+
const fromBin = resolveInterpreterFromBin(binPath, env);
|
|
102
|
+
if (fromBin)
|
|
103
|
+
candidates.push(fromBin);
|
|
104
|
+
}
|
|
105
|
+
candidates.push("/usr/bin/python3", "/usr/local/bin/python3", "/opt/homebrew/bin/python3");
|
|
29
106
|
for (const c of candidates) {
|
|
30
|
-
if (existsSync(c))
|
|
107
|
+
if (existsSync(c) && interpreterShipsSemgrep(c))
|
|
31
108
|
return c;
|
|
32
109
|
}
|
|
33
110
|
const probe = spawnSync("which", ["python3"], {
|
|
34
111
|
encoding: "utf8",
|
|
35
112
|
env: { ...env, PATH: env["PATH"] ?? "" },
|
|
36
113
|
});
|
|
37
|
-
if (probe.status === 0 && probe.stdout?.trim())
|
|
38
|
-
|
|
114
|
+
if (probe.status === 0 && probe.stdout?.trim()) {
|
|
115
|
+
const viaPath = probe.stdout.trim();
|
|
116
|
+
if (interpreterShipsSemgrep(viaPath))
|
|
117
|
+
return viaPath;
|
|
118
|
+
}
|
|
39
119
|
return null;
|
|
40
120
|
}
|
|
41
121
|
function findSemgrepBin(fallbackDirs) {
|
|
@@ -140,10 +220,12 @@ export async function runSemgrep(repo) {
|
|
|
140
220
|
reason: "semgrep binary not found on PATH; install semgrep or set SEMGREP_BIN",
|
|
141
221
|
};
|
|
142
222
|
}
|
|
143
|
-
// Locate the Python interpreter that ships the semgrep module
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
|
|
223
|
+
// Locate the Python interpreter that ships the semgrep module,
|
|
224
|
+
// preferring the interpreter named by the semgrep executable's own
|
|
225
|
+
// shebang line. An explicit SEMGREP_PYTHON is honored as-is; a
|
|
226
|
+
// set-but-missing value is invalid configuration, never a reason to
|
|
227
|
+
// silently fall back.
|
|
228
|
+
const pythonBin = resolvePythonForSemgrep(process.env, semgrepBin ?? undefined);
|
|
147
229
|
if (!pythonBin) {
|
|
148
230
|
cleanup();
|
|
149
231
|
const explicitPython = process.env["SEMGREP_PYTHON"];
|
|
@@ -157,7 +239,7 @@ export async function runSemgrep(repo) {
|
|
|
157
239
|
tool: "semgrep",
|
|
158
240
|
reason: explicitPython
|
|
159
241
|
? `SEMGREP_PYTHON is set to "${explicitPython}" but that path does not exist; set SEMGREP_PYTHON to a Python interpreter that ships the semgrep module`
|
|
160
|
-
:
|
|
242
|
+
: `No Python interpreter shipping the semgrep module was found (semgrep binary: "${semgrepBin}"); install semgrep so its executable resolves to a working interpreter, or set SEMGREP_PYTHON to a Python interpreter that ships the semgrep module`,
|
|
161
243
|
};
|
|
162
244
|
}
|
|
163
245
|
// Augment PATH so the inner Python process can find semgrep's
|
|
@@ -229,6 +311,7 @@ export async function runSemgrep(repo) {
|
|
|
229
311
|
duration: Date.now() - start,
|
|
230
312
|
status: "EXECUTION-ERROR",
|
|
231
313
|
tool: "semgrep",
|
|
314
|
+
reason: `semgrep exited with code ${proc.status} before producing findings (stderr: ${stderrHead}); this is a tool execution failure, not a rule match — see the SEM-CRASH finding`,
|
|
232
315
|
};
|
|
233
316
|
}
|
|
234
317
|
return {
|
package/dist/format.js
CHANGED
|
@@ -27,6 +27,19 @@ function statusBadge(cat) {
|
|
|
27
27
|
return cat.executed ? (cat.passed ? "PASS" : "FAIL") : "NOT-RUN";
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* The overall result line. A run that found no violations but could not
|
|
32
|
+
* execute a required stage is neither a pass nor a failure: it is
|
|
33
|
+
* incomplete. In particular the summary must never claim PASS while a
|
|
34
|
+
* category reports TOOL-UNAVAILABLE (the process exit code already
|
|
35
|
+
* distinguishes that case as TOOL_MISSING).
|
|
36
|
+
*/
|
|
37
|
+
function overallResult(result) {
|
|
38
|
+
if (!result.passed)
|
|
39
|
+
return "FAIL";
|
|
40
|
+
const toolUnavailable = Object.values(result.categories).some((cat) => cat.status === "TOOL-UNAVAILABLE");
|
|
41
|
+
return toolUnavailable ? "INCOMPLETE" : "PASS";
|
|
42
|
+
}
|
|
30
43
|
/**
|
|
31
44
|
* Format a unified summary for the terminal.
|
|
32
45
|
*/
|
|
@@ -56,7 +69,7 @@ export function formatSummary(result) {
|
|
|
56
69
|
}
|
|
57
70
|
}
|
|
58
71
|
lines.push("");
|
|
59
|
-
lines.push(`Result: ${result
|
|
72
|
+
lines.push(`Result: ${overallResult(result)}`);
|
|
60
73
|
return lines.join("\n");
|
|
61
74
|
}
|
|
62
75
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -6,4 +6,4 @@ export type { CategoryResult, RunOptions, RunResult } from "./run.js";
|
|
|
6
6
|
export { discoverRepository } from "./discover.js";
|
|
7
7
|
export { ExitCode } from "./exit-codes.js";
|
|
8
8
|
export { formatSummary, formatViolations } from "./format.js";
|
|
9
|
-
export { runValidation } from "./run.js";
|
|
9
|
+
export { exitCodeForResult, runValidation } from "./run.js";
|
package/dist/index.js
CHANGED
package/dist/run.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { EngineResult, Finding } from "./engines/types.js";
|
|
2
|
+
import type { ExitCodeValue } from "./exit-codes.js";
|
|
2
3
|
export interface RunOptions {
|
|
3
4
|
/** The root directory of the repository to validate */
|
|
4
5
|
root: string;
|
|
@@ -54,3 +55,12 @@ export interface RunResult {
|
|
|
54
55
|
* state. PASS without execution is impossible.
|
|
55
56
|
*/
|
|
56
57
|
export declare function runValidation(options: RunOptions): Promise<RunResult>;
|
|
58
|
+
/**
|
|
59
|
+
* Map a validation result to the documented process exit code.
|
|
60
|
+
*
|
|
61
|
+
* A required tool that could not run must never surface as PASS: when
|
|
62
|
+
* every executed engine passed but at least one category reports
|
|
63
|
+
* `TOOL-UNAVAILABLE`, the result exits `TOOL_MISSING` (3) instead of
|
|
64
|
+
* fabricating a passing result.
|
|
65
|
+
*/
|
|
66
|
+
export declare function exitCodeForResult(result: RunResult): ExitCodeValue;
|
package/dist/run.js
CHANGED
|
@@ -6,6 +6,7 @@ import { runDepcruise } from "./engines/depcruise.js";
|
|
|
6
6
|
import { runEslint } from "./engines/eslint.js";
|
|
7
7
|
import { runSchema } from "./engines/schema.js";
|
|
8
8
|
import { runSemgrep } from "./engines/semgrep.js";
|
|
9
|
+
import { ExitCode } from "./exit-codes.js";
|
|
9
10
|
function categoryOf(result) {
|
|
10
11
|
return {
|
|
11
12
|
status: result.status,
|
|
@@ -142,3 +143,17 @@ function emptyEngineResult(reason) {
|
|
|
142
143
|
reason,
|
|
143
144
|
};
|
|
144
145
|
}
|
|
146
|
+
/**
|
|
147
|
+
* Map a validation result to the documented process exit code.
|
|
148
|
+
*
|
|
149
|
+
* A required tool that could not run must never surface as PASS: when
|
|
150
|
+
* every executed engine passed but at least one category reports
|
|
151
|
+
* `TOOL-UNAVAILABLE`, the result exits `TOOL_MISSING` (3) instead of
|
|
152
|
+
* fabricating a passing result.
|
|
153
|
+
*/
|
|
154
|
+
export function exitCodeForResult(result) {
|
|
155
|
+
if (!result.passed)
|
|
156
|
+
return ExitCode.FAIL;
|
|
157
|
+
const toolUnavailable = Object.values(result.categories).some((c) => c.status === "TOOL-UNAVAILABLE");
|
|
158
|
+
return toolUnavailable ? ExitCode.TOOL_MISSING : ExitCode.PASS;
|
|
159
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@comity-dev/validate",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Thin orchestrator over Comity shared validation tooling. Development tooling.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"@comity-dev/schemas": "0.2.0",
|
|
38
38
|
"@comity-dev/dependency-rules": "0.2.0",
|
|
39
39
|
"@comity-dev/eslint-plugin": "0.2.0",
|
|
40
|
-
"@comity-dev/semgrep-rules": "0.2.
|
|
40
|
+
"@comity-dev/semgrep-rules": "0.2.1"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@types/node": "^24.13.3",
|