@opensip-cli/tool-gitleaks 0.1.15
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 +202 -0
- package/NOTICE +8 -0
- package/README.md +33 -0
- package/dist/__tests__/parse-gitleaks-json.test.d.ts +7 -0
- package/dist/__tests__/parse-gitleaks-json.test.d.ts.map +1 -0
- package/dist/__tests__/parse-gitleaks-json.test.js +100 -0
- package/dist/__tests__/parse-gitleaks-json.test.js.map +1 -0
- package/dist/__tests__/suite-step-e2e.test.d.ts +48 -0
- package/dist/__tests__/suite-step-e2e.test.d.ts.map +1 -0
- package/dist/__tests__/suite-step-e2e.test.js +262 -0
- package/dist/__tests__/suite-step-e2e.test.js.map +1 -0
- package/dist/__tests__/tool.test.d.ts +11 -0
- package/dist/__tests__/tool.test.d.ts.map +1 -0
- package/dist/__tests__/tool.test.js +197 -0
- package/dist/__tests__/tool.test.js.map +1 -0
- package/dist/__tests__/worker-e2e.test.d.ts +28 -0
- package/dist/__tests__/worker-e2e.test.d.ts.map +1 -0
- package/dist/__tests__/worker-e2e.test.js +425 -0
- package/dist/__tests__/worker-e2e.test.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/parse-gitleaks-json.d.ts +26 -0
- package/dist/parse-gitleaks-json.d.ts.map +1 -0
- package/dist/parse-gitleaks-json.js +88 -0
- package/dist/parse-gitleaks-json.js.map +1 -0
- package/dist/tool.d.ts +74 -0
- package/dist/tool.d.ts.map +1 -0
- package/dist/tool.js +140 -0
- package/dist/tool.js.map +1 -0
- package/package.json +142 -0
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tier-2 worker E2E (ADR-0090 D6 Tier 2) — install / discover / dispatch the
|
|
3
|
+
* gitleaks adapter over a REAL forked worker, end-to-end against the BUILT CLI.
|
|
4
|
+
*
|
|
5
|
+
* Unlike the in-process Tier-1 suites, this proves the FULL installed-tool path:
|
|
6
|
+
* - the gitleaks package is presented as a genuinely INSTALLED npm tool in a
|
|
7
|
+
* throwaway project (symlinked into its `node_modules` so the worker resolves
|
|
8
|
+
* the adapter's workspace deps from the monorepo via realpath);
|
|
9
|
+
* - `OPENSIP_CLI_ALLOW_INSTALLED_TOOLS` trusts it (installed tools are
|
|
10
|
+
* deny-by-default);
|
|
11
|
+
* - a FAKE `gitleaks` binary on PATH makes the run deterministic (it copies the
|
|
12
|
+
* committed golden to `--report-path` and exits 1, like real gitleaks on
|
|
13
|
+
* findings). The worker fork curates its env to an allow-list, so the golden
|
|
14
|
+
* path is forwarded via the documented `OPENSIP_CLI_TOOL_ENV_PASSTHROUGH`.
|
|
15
|
+
*
|
|
16
|
+
* `opensip gitleaks` forks a worker that re-discovers + imports the real runtime
|
|
17
|
+
* and runs the scan loop; this suite asserts the worker→host result + the
|
|
18
|
+
* host-side effects: normalized signals match the golden, the raw artifact lands
|
|
19
|
+
* at `.runtime/artifacts/gitleaks/<runId>/gitleaks.json` with mode 0600, the
|
|
20
|
+
* `--json` envelope is well-formed, the session row persists with provenance, and
|
|
21
|
+
* — the load-bearing negative — NO raw `Secret`/`Match` substring ever reaches the
|
|
22
|
+
* emitted worker→host payload.
|
|
23
|
+
*
|
|
24
|
+
* Requires `pnpm build` first (the CLI dist + the gitleaks dist). Missing builds
|
|
25
|
+
* FAIL loudly (no silent skip).
|
|
26
|
+
*/
|
|
27
|
+
import { execFileSync } from 'node:child_process';
|
|
28
|
+
import { cpSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync, symlinkSync, writeFileSync, } from 'node:fs';
|
|
29
|
+
import { tmpdir } from 'node:os';
|
|
30
|
+
import { dirname, join } from 'node:path';
|
|
31
|
+
import { fileURLToPath } from 'node:url';
|
|
32
|
+
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
33
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
34
|
+
// .../packages/tool-gitleaks/src/__tests__ → repo root is four levels up.
|
|
35
|
+
const REPO_ROOT = join(HERE, '..', '..', '..', '..');
|
|
36
|
+
const CLI_DIST = join(REPO_ROOT, 'packages', 'cli', 'dist', 'index.js');
|
|
37
|
+
const GITLEAKS_PKG_DIR = join(REPO_ROOT, 'packages', 'tool-gitleaks');
|
|
38
|
+
const FIXTURES = join(GITLEAKS_PKG_DIR, '__fixtures__');
|
|
39
|
+
const GOLDEN_PATH = join(FIXTURES, 'gitleaks-golden.json');
|
|
40
|
+
const GITLEAKS_STABLE_ID = 'cd08f737-ce8e-4813-9259-b4ffeb954268';
|
|
41
|
+
const EXPECTED = JSON.parse(readFileSync(join(FIXTURES, 'expected-signals.json'), 'utf8'));
|
|
42
|
+
// The raw matched-credential strings that must NEVER reach the emitted payload.
|
|
43
|
+
const RAW_SECRETS = ['AKIAIOSFODNN7EXAMPLE', 'glpat-XXXXXXXXXXXXXXXXXXXX', 'aws_key ='];
|
|
44
|
+
let projectDir;
|
|
45
|
+
let binDir;
|
|
46
|
+
let baseEnv;
|
|
47
|
+
/** Run the built CLI as a child process, capturing stdout/stderr + exit code. */
|
|
48
|
+
function runCli(args, extraEnv = {}, cwd = projectDir) {
|
|
49
|
+
try {
|
|
50
|
+
const stdout = execFileSync('node', [CLI_DIST, ...args], {
|
|
51
|
+
cwd,
|
|
52
|
+
env: { ...process.env, ...baseEnv, ...extraEnv },
|
|
53
|
+
encoding: 'utf8',
|
|
54
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
55
|
+
});
|
|
56
|
+
return { stdout, stderr: '', status: 0 };
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
const e = error;
|
|
60
|
+
return { stdout: e.stdout ?? '', stderr: e.stderr ?? '', status: e.status ?? 1 };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/** Scaffold a throwaway opensip-cli project that resolves the installed gitleaks tool. */
|
|
64
|
+
function makeGitleaksProject() {
|
|
65
|
+
const dir = mkdtempSync(join(tmpdir(), 'opensip-gitleaks-gate-'));
|
|
66
|
+
writeFileSync(join(dir, 'opensip-cli.config.yml'), 'schemaVersion: 1\ntargets: {}\n', 'utf8');
|
|
67
|
+
const scopeDir = join(dir, 'node_modules', '@opensip-cli');
|
|
68
|
+
mkdirSync(scopeDir, { recursive: true });
|
|
69
|
+
symlinkSync(GITLEAKS_PKG_DIR, join(scopeDir, 'tool-gitleaks'), 'dir');
|
|
70
|
+
return dir;
|
|
71
|
+
}
|
|
72
|
+
/** Parse the `--json` outcome wrapper (`{ kind, status, exitCode, envelope?, data? }`). */
|
|
73
|
+
function parseOutcome(stdout) {
|
|
74
|
+
return JSON.parse(stdout);
|
|
75
|
+
}
|
|
76
|
+
beforeAll(() => {
|
|
77
|
+
if (!existsSync(CLI_DIST)) {
|
|
78
|
+
throw new Error(`built CLI not found at ${CLI_DIST} — run \`pnpm build\` first`);
|
|
79
|
+
}
|
|
80
|
+
if (!existsSync(join(GITLEAKS_PKG_DIR, 'dist', 'index.js'))) {
|
|
81
|
+
throw new Error('built tool-gitleaks dist not found — run `pnpm build` first');
|
|
82
|
+
}
|
|
83
|
+
projectDir = mkdtempSync(join(tmpdir(), 'opensip-gitleaks-e2e-'));
|
|
84
|
+
// Project marker so the worker's `scope: 'project'` bootstrap resolves a project.
|
|
85
|
+
writeFileSync(join(projectDir, 'opensip-cli.config.yml'), 'schemaVersion: 1\ntargets: {}\n', 'utf8');
|
|
86
|
+
// Present the REAL gitleaks package as an installed npm tool. A SYMLINK (not a
|
|
87
|
+
// copy) so the worker resolves the adapter's `@opensip-cli/*` workspace deps
|
|
88
|
+
// from the monorepo via realpath — a copy would orphan them.
|
|
89
|
+
const scopeDir = join(projectDir, 'node_modules', '@opensip-cli');
|
|
90
|
+
mkdirSync(scopeDir, { recursive: true });
|
|
91
|
+
symlinkSync(GITLEAKS_PKG_DIR, join(scopeDir, 'tool-gitleaks'), 'dir');
|
|
92
|
+
// A FAKE gitleaks on PATH for determinism (copies the golden to --report-path,
|
|
93
|
+
// exits 1). PATH is auto-forwarded into the worker fork's curated env.
|
|
94
|
+
binDir = mkdtempSync(join(tmpdir(), 'opensip-gitleaks-bin-'));
|
|
95
|
+
cpSync(join(FIXTURES, 'fake-gitleaks'), join(binDir, 'gitleaks'));
|
|
96
|
+
execFileSync('chmod', ['+x', join(binDir, 'gitleaks')]);
|
|
97
|
+
baseEnv = {
|
|
98
|
+
PATH: `${binDir}:${process.env.PATH ?? ''}`,
|
|
99
|
+
// The committed fake binary reads the golden path from here; forward it
|
|
100
|
+
// through the worker fork's env allow-list via the documented passthrough.
|
|
101
|
+
FAKE_GITLEAKS_GOLDEN: GOLDEN_PATH,
|
|
102
|
+
OPENSIP_CLI_TOOL_ENV_PASSTHROUGH: 'FAKE_GITLEAKS_GOLDEN',
|
|
103
|
+
// Installed tools are deny-by-default — trust the gitleaks id (the admission
|
|
104
|
+
// check keys on `opensipTools.id`; the UUID is included to match the
|
|
105
|
+
// ADR-0048 stable id convention).
|
|
106
|
+
OPENSIP_CLI_ALLOW_INSTALLED_TOOLS: `${GITLEAKS_STABLE_ID} gitleaks`,
|
|
107
|
+
};
|
|
108
|
+
});
|
|
109
|
+
afterAll(() => {
|
|
110
|
+
if (projectDir !== undefined)
|
|
111
|
+
rmSync(projectDir, { recursive: true, force: true });
|
|
112
|
+
if (binDir !== undefined)
|
|
113
|
+
rmSync(binDir, { recursive: true, force: true });
|
|
114
|
+
});
|
|
115
|
+
describe('gitleaks worker E2E — opensip gitleaks (real forked worker)', () => {
|
|
116
|
+
let scan;
|
|
117
|
+
let envelope;
|
|
118
|
+
beforeAll(() => {
|
|
119
|
+
scan = runCli(['gitleaks', '--json']);
|
|
120
|
+
const outcome = parseOutcome(scan.stdout);
|
|
121
|
+
envelope = outcome.envelope;
|
|
122
|
+
});
|
|
123
|
+
it('forks a worker and emits a well-formed signal envelope for gitleaks', () => {
|
|
124
|
+
expect(envelope.tool).toBe('gitleaks');
|
|
125
|
+
expect(envelope.runId).toMatch(/^RUN_/);
|
|
126
|
+
// High-severity secrets ⇒ the run FAILS; the gate exit is non-zero (findings).
|
|
127
|
+
expect(envelope.verdict.passed).toBe(false);
|
|
128
|
+
expect(scan.status).toBe(1);
|
|
129
|
+
});
|
|
130
|
+
it('normalizes the worker scan output to the golden signal shapes', () => {
|
|
131
|
+
const shapes = envelope.signals.map((s) => ({
|
|
132
|
+
ruleId: s.ruleId,
|
|
133
|
+
severity: s.severity,
|
|
134
|
+
message: s.message,
|
|
135
|
+
file: s.filePath,
|
|
136
|
+
...(s.line === undefined ? {} : { line: s.line }),
|
|
137
|
+
...(s.column === undefined ? {} : { column: s.column }),
|
|
138
|
+
}));
|
|
139
|
+
expect(shapes).toEqual(EXPECTED);
|
|
140
|
+
});
|
|
141
|
+
it('stamps message-hash fingerprints + provenance worker-side', () => {
|
|
142
|
+
for (const s of envelope.signals) {
|
|
143
|
+
expect(s.fingerprint).toMatch(/^[0-9a-f]{64}$/);
|
|
144
|
+
const provenance = s.metadata.provenance;
|
|
145
|
+
expect(provenance.tool).toBe('gitleaks');
|
|
146
|
+
expect(provenance.adapterPackage).toBe('@opensip-cli/tool-gitleaks');
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
it('lands the raw artifact under .runtime/artifacts/gitleaks/<runId>/gitleaks.json with mode 0600 in a 0700 dir', () => {
|
|
150
|
+
const runDir = join(projectDir, 'opensip-cli', '.runtime', 'artifacts', 'gitleaks');
|
|
151
|
+
expect(existsSync(runDir)).toBe(true);
|
|
152
|
+
const runs = readdirSync(runDir);
|
|
153
|
+
expect(runs.length).toBeGreaterThan(0);
|
|
154
|
+
const perRunDir = join(runDir, runs[0]);
|
|
155
|
+
const artifact = join(perRunDir, 'gitleaks.json');
|
|
156
|
+
// A1: the fake binary does NOT `mkdir -p` its --report-path dir — this artifact
|
|
157
|
+
// exists only because the host `ensureArtifactDir` seam created the per-run dir
|
|
158
|
+
// BEFORE the scan. A missing seam would ENOENT the fake's `cp` (no artifact).
|
|
159
|
+
expect(existsSync(artifact)).toBe(true);
|
|
160
|
+
// A7: the per-run dir is owner-only (0700) so a scanner's umask-default report is
|
|
161
|
+
// not world-traversable; the artifact file itself is 0600 (host atomic re-write).
|
|
162
|
+
expect(statSync(perRunDir).mode & 0o777).toBe(0o700);
|
|
163
|
+
expect(statSync(artifact).mode & 0o777).toBe(0o600);
|
|
164
|
+
// The persisted artifact is the byte-preserved golden.
|
|
165
|
+
expect(JSON.parse(readFileSync(artifact, 'utf8'))).toHaveLength(2);
|
|
166
|
+
});
|
|
167
|
+
it('NEVER lets a raw Secret/Match substring reach the emitted worker→host payload', () => {
|
|
168
|
+
for (const raw of RAW_SECRETS) {
|
|
169
|
+
expect(scan.stdout).not.toContain(raw);
|
|
170
|
+
}
|
|
171
|
+
expect(scan.stdout).not.toContain('"Match"');
|
|
172
|
+
// The masked preview IS present (the finding stays identifiable).
|
|
173
|
+
expect(scan.stdout).toContain('AKIA…');
|
|
174
|
+
expect(scan.stdout).toContain('glpa…');
|
|
175
|
+
});
|
|
176
|
+
it('persists a session row with the gitleaks tool + provenance payload', () => {
|
|
177
|
+
const list = runCli(['sessions', 'list', '--json']);
|
|
178
|
+
expect(list.status).toBe(0);
|
|
179
|
+
const outcome = parseOutcome(list.stdout);
|
|
180
|
+
const data = outcome.data;
|
|
181
|
+
const sessions = data?.sessions ?? [];
|
|
182
|
+
const gitleaksRow = sessions.find((s) => s.tool === 'gitleaks');
|
|
183
|
+
expect(gitleaksRow).toBeDefined();
|
|
184
|
+
expect(gitleaksRow?.passed).toBe(false);
|
|
185
|
+
const payload = gitleaksRow?.payload;
|
|
186
|
+
expect(payload?.binary?.path).toContain('gitleaks');
|
|
187
|
+
expect(payload?.findings).toBe(2);
|
|
188
|
+
});
|
|
189
|
+
// A2: the session payload must carry grouped `checks[]` + a populated `summary`
|
|
190
|
+
// so the HTML report renders the leaked credentials instead of "this run was
|
|
191
|
+
// clean. Every rule passed" (the dashboard groups `payload.checks[]` and reads
|
|
192
|
+
// `payload.summary` for its clean/dirty decision).
|
|
193
|
+
it('persists a grouped session payload (checks[] + summary) so the report is NOT falsely clean', () => {
|
|
194
|
+
const list = runCli(['sessions', 'list', '--json']);
|
|
195
|
+
const data = parseOutcome(list.stdout).data;
|
|
196
|
+
const gitleaksRow = (data.sessions ?? []).find((s) => s.tool === 'gitleaks');
|
|
197
|
+
const payload = gitleaksRow?.payload;
|
|
198
|
+
// Grouped per-rule detail is present (the two golden secrets are two rules).
|
|
199
|
+
expect(Array.isArray(payload?.checks)).toBe(true);
|
|
200
|
+
expect(payload?.checks?.length).toBe(2);
|
|
201
|
+
expect(payload?.checks?.every((c) => c.passed === false)).toBe(true);
|
|
202
|
+
// The dashboard's clean test (`errors === 0 && warnings === 0`) is FALSE here.
|
|
203
|
+
expect(payload?.summary?.errors).toBe(2);
|
|
204
|
+
const clean = (payload?.summary?.errors ?? 0) === 0 && (payload?.summary?.warnings ?? 0) === 0;
|
|
205
|
+
expect(clean).toBe(false);
|
|
206
|
+
});
|
|
207
|
+
// A2 secret hygiene: the newly-persisted per-finding text must NOT leak a raw
|
|
208
|
+
// credential into the stored payload (redaction happens at ingest, before the
|
|
209
|
+
// envelope/payload build — prove it holds through to the persisted row).
|
|
210
|
+
it('NEVER persists a raw Secret/Match into the session payload (only the masked preview)', () => {
|
|
211
|
+
const list = runCli(['sessions', 'list', '--json']);
|
|
212
|
+
const data = parseOutcome(list.stdout).data;
|
|
213
|
+
const gitleaksRow = (data.sessions ?? []).find((s) => s.tool === 'gitleaks');
|
|
214
|
+
const payloadBlob = JSON.stringify(gitleaksRow?.payload ?? {});
|
|
215
|
+
for (const raw of RAW_SECRETS)
|
|
216
|
+
expect(payloadBlob).not.toContain(raw);
|
|
217
|
+
expect(payloadBlob).not.toContain('"Match"');
|
|
218
|
+
expect(payloadBlob).not.toContain('"Secret"');
|
|
219
|
+
// The masked preview survives so the finding stays identifiable in the report.
|
|
220
|
+
expect(payloadBlob).toContain('AKIA…');
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
describe('gitleaks worker E2E — doctor / version diagnostics', () => {
|
|
224
|
+
it('doctor --json reports a ready, resolved binary (exit 0)', () => {
|
|
225
|
+
const run = runCli(['gitleaks', 'doctor', '--json']);
|
|
226
|
+
expect(run.status).toBe(0);
|
|
227
|
+
const report = parseOutcome(run.stdout).data;
|
|
228
|
+
expect(report.tool).toBe('gitleaks');
|
|
229
|
+
expect(report.ready).toBe(true);
|
|
230
|
+
expect(report.binary.found).toBe(true);
|
|
231
|
+
expect(report.version.detected).toBe('8.18.4');
|
|
232
|
+
expect(report.version.status).toBe('ok');
|
|
233
|
+
});
|
|
234
|
+
it('doctor reports NOT ready (exit 2) when the resolved binary is missing', () => {
|
|
235
|
+
// Pin the binary to a non-existent absolute path via the env layer (which
|
|
236
|
+
// beats PATH and hard-misses) so resolution fails WITHOUT breaking the
|
|
237
|
+
// toolchain/worker fork. Forward the pin into the worker (doctor probes
|
|
238
|
+
// worker-side) via the documented passthrough.
|
|
239
|
+
const run = runCli(['gitleaks', 'doctor', '--json'], {
|
|
240
|
+
OPENSIP_GITLEAKS_BIN: '/nonexistent/path/to/gitleaks',
|
|
241
|
+
OPENSIP_CLI_TOOL_ENV_PASSTHROUGH: 'FAKE_GITLEAKS_GOLDEN OPENSIP_GITLEAKS_BIN',
|
|
242
|
+
});
|
|
243
|
+
expect(run.status).toBe(2);
|
|
244
|
+
const report = parseOutcome(run.stdout).data;
|
|
245
|
+
expect(report.ready).toBe(false);
|
|
246
|
+
expect(report.binary.found).toBe(false);
|
|
247
|
+
});
|
|
248
|
+
it('version --json prints the resolved gitleaks binary version', () => {
|
|
249
|
+
const run = runCli(['gitleaks', 'version', '--json']);
|
|
250
|
+
expect(run.status).toBe(0);
|
|
251
|
+
const report = parseOutcome(run.stdout).data;
|
|
252
|
+
expect(report.found).toBe(true);
|
|
253
|
+
expect(report.version).toBe('8.18.4');
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
/**
|
|
257
|
+
* A5/A6 — the typed exit-class must survive the worker boundary. The scan handler
|
|
258
|
+
* runs in a FORKED worker; a `ConfigurationError` it throws (binary-not-found) must
|
|
259
|
+
* arrive at the host as exit 2, the SAME contract `doctor` and the in-process
|
|
260
|
+
* bundled path honour. Before the fix the worker IPC flattened the typed error to
|
|
261
|
+
* a generic `tool-handler-throw` → SystemError → exit 1, silently losing the
|
|
262
|
+
* frozen exit-2 contract; NO E2E asserted the scan exit code.
|
|
263
|
+
*/
|
|
264
|
+
describe('gitleaks worker E2E — typed exit-class survives the worker boundary (A5/A6)', () => {
|
|
265
|
+
it('`opensip gitleaks` with a binary-not-found ConfigurationError exits 2 (not 1)', () => {
|
|
266
|
+
// Pin the binary to a non-existent absolute path so resolution hard-misses
|
|
267
|
+
// worker-side and the run loop throws ConfigurationError(ADAPTER.BINARY.NOT_FOUND).
|
|
268
|
+
const run = runCli(['gitleaks', '--json'], {
|
|
269
|
+
OPENSIP_GITLEAKS_BIN: '/nonexistent/path/to/gitleaks',
|
|
270
|
+
OPENSIP_CLI_TOOL_ENV_PASSTHROUGH: 'FAKE_GITLEAKS_GOLDEN OPENSIP_GITLEAKS_BIN',
|
|
271
|
+
});
|
|
272
|
+
expect(run.status).toBe(2);
|
|
273
|
+
});
|
|
274
|
+
it('`opensip gitleaks` with no opensip-cli project exits 2 (not 1)', () => {
|
|
275
|
+
// A project-less run has no targeting root to scan — the exit-2 config contract.
|
|
276
|
+
const noProject = mkdtempSync(join(tmpdir(), 'opensip-gitleaks-noproj-'));
|
|
277
|
+
const scopeDir = join(noProject, 'node_modules', '@opensip-cli');
|
|
278
|
+
mkdirSync(scopeDir, { recursive: true });
|
|
279
|
+
symlinkSync(GITLEAKS_PKG_DIR, join(scopeDir, 'tool-gitleaks'), 'dir');
|
|
280
|
+
try {
|
|
281
|
+
const run = runCli(['gitleaks', '--json'], {}, noProject);
|
|
282
|
+
expect(run.status).toBe(2);
|
|
283
|
+
}
|
|
284
|
+
finally {
|
|
285
|
+
rmSync(noProject, { recursive: true, force: true });
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
describe('gitleaks worker E2E — installed tools are deny-by-default', () => {
|
|
290
|
+
it('without the trust allowlist, `opensip gitleaks` is not admitted', () => {
|
|
291
|
+
const run = runCli(['gitleaks', '--json'], { OPENSIP_CLI_ALLOW_INSTALLED_TOOLS: '' });
|
|
292
|
+
// Deny-by-default: the command never mounts (unknown command / not found),
|
|
293
|
+
// so the scan does NOT run.
|
|
294
|
+
expect(run.status).not.toBe(0);
|
|
295
|
+
expect(`${run.stdout}${run.stderr}`.toLowerCase()).toMatch(/unknown command|not found|gitleaks/);
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
/**
|
|
299
|
+
* §4.12 ratchet acceptance — the FULL baseline/gate loop over a REAL forked worker.
|
|
300
|
+
* The substrate wires `--gate-save` / `--gate-compare` once (ADR-0036), so the
|
|
301
|
+
* gitleaks adapter inherits it. This proves: capture a baseline → an unchanged
|
|
302
|
+
* re-scan is clean (exit 0) → a NET-NEW secret surfaces and fails (exit ≠ 0).
|
|
303
|
+
*
|
|
304
|
+
* Runs in its OWN throwaway project so the baseline + sessions are isolated from
|
|
305
|
+
* the scan suites above. The fake binary copies `FAKE_GITLEAKS_GOLDEN` to
|
|
306
|
+
* `--report-path`; pointing it at an AUGMENTED golden (the two originals + one new
|
|
307
|
+
* finding) is how the "regression" run injects a net-new finding.
|
|
308
|
+
*/
|
|
309
|
+
describe('gitleaks worker E2E — full gate ratchet (§4.12)', () => {
|
|
310
|
+
let gateProject;
|
|
311
|
+
let augmentedGolden;
|
|
312
|
+
let save;
|
|
313
|
+
let compareClean;
|
|
314
|
+
let compareRegressed;
|
|
315
|
+
beforeAll(() => {
|
|
316
|
+
gateProject = makeGitleaksProject();
|
|
317
|
+
// The augmented golden = the committed two findings + one NET-NEW secret in a
|
|
318
|
+
// new file/rule (a distinct message-hash fingerprint ⇒ the ratchet sees it as
|
|
319
|
+
// net-new, not unchanged).
|
|
320
|
+
const original = JSON.parse(readFileSync(GOLDEN_PATH, 'utf8'));
|
|
321
|
+
const augmented = [
|
|
322
|
+
...original,
|
|
323
|
+
{
|
|
324
|
+
Description: 'Stripe Access Token',
|
|
325
|
+
StartLine: 7,
|
|
326
|
+
EndLine: 7,
|
|
327
|
+
StartColumn: 14,
|
|
328
|
+
EndColumn: 45,
|
|
329
|
+
Match: 'stripe = sk_live_NEWFAKEKEYDONOTUSE',
|
|
330
|
+
Secret: 'sk_live_NEWFAKEKEYDONOTUSE',
|
|
331
|
+
File: 'src/payments.ts',
|
|
332
|
+
RuleID: 'stripe-access-token',
|
|
333
|
+
Tags: [],
|
|
334
|
+
Fingerprint: 'src/payments.ts:stripe-access-token:7',
|
|
335
|
+
},
|
|
336
|
+
];
|
|
337
|
+
augmentedGolden = join(gateProject, 'augmented-golden.json');
|
|
338
|
+
writeFileSync(augmentedGolden, JSON.stringify(augmented), 'utf8');
|
|
339
|
+
// 1) Capture the baseline (two findings). The findings gate (ADR-0020) makes
|
|
340
|
+
// gate-save itself exit 1 — it records the baseline AND honours the verdict.
|
|
341
|
+
save = runCli(['gitleaks', '--gate-save'], {}, gateProject);
|
|
342
|
+
// 2) Re-scan the SAME golden and compare → no net-new ⇒ clean (exit 0).
|
|
343
|
+
compareClean = runCli(['gitleaks', '--gate-compare'], {}, gateProject);
|
|
344
|
+
// 3) Compare against the augmented golden → one net-new finding ⇒ degraded.
|
|
345
|
+
compareRegressed = runCli(['gitleaks', '--gate-compare'], { FAKE_GITLEAKS_GOLDEN: augmentedGolden }, gateProject);
|
|
346
|
+
});
|
|
347
|
+
afterAll(() => {
|
|
348
|
+
if (gateProject !== undefined)
|
|
349
|
+
rmSync(gateProject, { recursive: true, force: true });
|
|
350
|
+
});
|
|
351
|
+
it('--gate-save records the baseline and persists a session', () => {
|
|
352
|
+
// The findings gate makes gate-save exit 1 (two high-severity secrets present),
|
|
353
|
+
// but the baseline IS written — proven by the clean compare below.
|
|
354
|
+
expect(save.status).toBe(1);
|
|
355
|
+
const list = runCli(['sessions', 'list', '--json'], {}, gateProject);
|
|
356
|
+
const data = parseOutcome(list.stdout).data;
|
|
357
|
+
const gitleaksRows = (data.sessions ?? []).filter((s) => s.tool === 'gitleaks');
|
|
358
|
+
expect(gitleaksRows.length).toBeGreaterThanOrEqual(1);
|
|
359
|
+
});
|
|
360
|
+
it('--gate-compare on the SAME scan is a clean no-op (exit 0, no regression)', () => {
|
|
361
|
+
// Pre-existing findings recorded in the baseline are NOT a regression — only
|
|
362
|
+
// net-new findings fail the ratchet. The clean exit also proves gate-save wrote
|
|
363
|
+
// the baseline (a missing baseline would throw ConfigurationError → exit 2).
|
|
364
|
+
expect(compareRegressed).toBeDefined();
|
|
365
|
+
expect(compareClean.status).toBe(0);
|
|
366
|
+
expect(`${compareClean.stdout}${compareClean.stderr}`).toMatch(/STABLE|no change/i);
|
|
367
|
+
});
|
|
368
|
+
it('--gate-compare surfaces a NET-NEW finding and exits non-zero (degraded)', () => {
|
|
369
|
+
expect(compareRegressed.status).not.toBe(0);
|
|
370
|
+
const out = `${compareRegressed.stdout}${compareRegressed.stderr}`;
|
|
371
|
+
// The net-new secret is named in the diff; the verdict footer says DEGRADED.
|
|
372
|
+
expect(out).toContain('stripe-access-token');
|
|
373
|
+
expect(out).toMatch(/DEGRADED|Added/i);
|
|
374
|
+
// The raw secret never leaks into the gate output.
|
|
375
|
+
expect(out).not.toContain('sk_live_NEWFAKEKEYDONOTUSE');
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
/**
|
|
379
|
+
* A3 acceptance — the scanner must NEVER re-walk opensip's own persisted reports
|
|
380
|
+
* under `.runtime/`. Uses a WALKING fake that actually scans `--source` and
|
|
381
|
+
* re-detects `OPENSIP_TEST_SECRET` in any non-excluded file (so a prior run's
|
|
382
|
+
* report would mint a net-new fingerprint), honoring the SAME `--config` allowlist
|
|
383
|
+
* marker the substrate injects. Without the A3 fix the first `--gate-compare`
|
|
384
|
+
* re-detects the gate-save run's report (a new runId path) and degrades; with it,
|
|
385
|
+
* two consecutive compares over an UNCHANGED project stay clean.
|
|
386
|
+
*/
|
|
387
|
+
describe('gitleaks worker E2E — A3 no-churn over the .runtime artifact store', () => {
|
|
388
|
+
let churnProject;
|
|
389
|
+
let walkBinDir;
|
|
390
|
+
let save;
|
|
391
|
+
let compare1;
|
|
392
|
+
let compare2;
|
|
393
|
+
beforeAll(() => {
|
|
394
|
+
walkBinDir = mkdtempSync(join(tmpdir(), 'opensip-gitleaks-walk-bin-'));
|
|
395
|
+
cpSync(join(FIXTURES, 'fake-gitleaks-walking'), join(walkBinDir, 'gitleaks'));
|
|
396
|
+
execFileSync('chmod', ['+x', join(walkBinDir, 'gitleaks')]);
|
|
397
|
+
churnProject = makeGitleaksProject();
|
|
398
|
+
// Exactly one real secret planted in the project source tree.
|
|
399
|
+
mkdirSync(join(churnProject, 'src'), { recursive: true });
|
|
400
|
+
writeFileSync(join(churnProject, 'src', 'leak.txt'), 'token = OPENSIP_TEST_SECRET\n', 'utf8');
|
|
401
|
+
// Override PATH so the WALKING fake is the resolved `gitleaks`.
|
|
402
|
+
const env = { PATH: `${walkBinDir}:${process.env.PATH ?? ''}` };
|
|
403
|
+
save = runCli(['gitleaks', '--gate-save'], env, churnProject);
|
|
404
|
+
compare1 = runCli(['gitleaks', '--gate-compare'], env, churnProject);
|
|
405
|
+
compare2 = runCli(['gitleaks', '--gate-compare'], env, churnProject);
|
|
406
|
+
});
|
|
407
|
+
afterAll(() => {
|
|
408
|
+
if (churnProject !== undefined)
|
|
409
|
+
rmSync(churnProject, { recursive: true, force: true });
|
|
410
|
+
if (walkBinDir !== undefined)
|
|
411
|
+
rmSync(walkBinDir, { recursive: true, force: true });
|
|
412
|
+
});
|
|
413
|
+
it('captures a baseline of exactly the planted secret (gate-save exits 1 on findings)', () => {
|
|
414
|
+
expect(save.status).toBe(1);
|
|
415
|
+
});
|
|
416
|
+
it('two consecutive --gate-compare cycles over the UNCHANGED project stay clean (exit 0)', () => {
|
|
417
|
+
// Without the A3 exclusion, each compare re-detects the prior run's persisted
|
|
418
|
+
// report under `.runtime/` (a NEW runId path ⇒ net-new fingerprint ⇒ degraded,
|
|
419
|
+
// exit ≠ 0). The exclusion keeps the scanner off `.runtime`, so the only finding
|
|
420
|
+
// is the unchanged planted secret → no net-new → exit 0, repeatably.
|
|
421
|
+
expect(compare1.status).toBe(0);
|
|
422
|
+
expect(compare2.status).toBe(0);
|
|
423
|
+
});
|
|
424
|
+
});
|
|
425
|
+
//# sourceMappingURL=worker-e2e.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"worker-e2e.test.js","sourceRoot":"","sources":["../../src/__tests__/worker-e2e.test.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@opensip-cli/tool-gitleaks` public barrel.
|
|
3
|
+
*
|
|
4
|
+
* Re-exports the `tool` descriptor the host loads by name through the installed
|
|
5
|
+
* external-tool worker-dispatch path (`mod.tool`), plus a `default` alias and the
|
|
6
|
+
* pure `parseGitleaksJson` normalizer (consumed by the acceptance tests). The
|
|
7
|
+
* adapter internals are otherwise not public API.
|
|
8
|
+
*/
|
|
9
|
+
export { tool, tool as default, GITLEAKS_IDENTITY, GITLEAKS_STABLE_ID } from './tool.js';
|
|
10
|
+
export { parseGitleaksJson } from './parse-gitleaks-json.js';
|
|
11
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,IAAI,EAAE,IAAI,IAAI,OAAO,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AACzF,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@opensip-cli/tool-gitleaks` public barrel.
|
|
3
|
+
*
|
|
4
|
+
* Re-exports the `tool` descriptor the host loads by name through the installed
|
|
5
|
+
* external-tool worker-dispatch path (`mod.tool`), plus a `default` alias and the
|
|
6
|
+
* pure `parseGitleaksJson` normalizer (consumed by the acceptance tests). The
|
|
7
|
+
* adapter internals are otherwise not public API.
|
|
8
|
+
*/
|
|
9
|
+
export { tool, tool as default, GITLEAKS_IDENTITY, GITLEAKS_STABLE_ID } from './tool.js';
|
|
10
|
+
export { parseGitleaksJson } from './parse-gitleaks-json.js';
|
|
11
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,IAAI,EAAE,IAAI,IAAI,OAAO,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AACzF,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Gitleaks JSON → normalized `Signal[]` (ADR-0090 §4, brief §4).
|
|
3
|
+
*
|
|
4
|
+
* Gitleaks emits a BARE JSON ARRAY of findings (`[]` when clean). Each finding
|
|
5
|
+
* carries the live credential in two fields — `Secret` (the raw secret) and
|
|
6
|
+
* `Match` (the surrounding region, which INCLUDES the secret). Neither may EVER
|
|
7
|
+
* reach `Signal.message`, `Signal.metadata` in raw form, or any egress payload:
|
|
8
|
+
* this parser stores ONLY a {@link redactSecret} preview of `Secret` and drops
|
|
9
|
+
* `Match` entirely. The secret-egress negative test proves no raw substring
|
|
10
|
+
* survives.
|
|
11
|
+
*
|
|
12
|
+
* Stock gitleaks emits NO severity (every secret is treated as `high`); the
|
|
13
|
+
* native-severity slot records `null` so a downstream reader knows the four-bucket
|
|
14
|
+
* `high` is the adapter's constant map, not a scanner label.
|
|
15
|
+
*
|
|
16
|
+
* Pure: defensive JSON navigation (never throws on malformed input → `[]`).
|
|
17
|
+
*/
|
|
18
|
+
import type { Signal } from '@opensip-cli/core';
|
|
19
|
+
import type { AdapterRunContext, ParsedScannerOutput } from '@opensip-cli/external-tool-adapter';
|
|
20
|
+
/**
|
|
21
|
+
* Parse gitleaks JSON output into normalized signals. A clean run (`[]`) yields
|
|
22
|
+
* no findings; every secret maps to a `high`-severity `security` signal with a
|
|
23
|
+
* REDACTED secret preview and no raw credential anywhere.
|
|
24
|
+
*/
|
|
25
|
+
export declare function parseGitleaksJson(raw: ParsedScannerOutput, _ctx: AdapterRunContext): readonly Signal[];
|
|
26
|
+
//# sourceMappingURL=parse-gitleaks-json.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parse-gitleaks-json.d.ts","sourceRoot":"","sources":["../src/parse-gitleaks-json.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAYH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,KAAK,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,oCAAoC,CAAC;AA+DjG;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,mBAAmB,EACxB,IAAI,EAAE,iBAAiB,GACtB,SAAS,MAAM,EAAE,CAOnB"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Gitleaks JSON → normalized `Signal[]` (ADR-0090 §4, brief §4).
|
|
3
|
+
*
|
|
4
|
+
* Gitleaks emits a BARE JSON ARRAY of findings (`[]` when clean). Each finding
|
|
5
|
+
* carries the live credential in two fields — `Secret` (the raw secret) and
|
|
6
|
+
* `Match` (the surrounding region, which INCLUDES the secret). Neither may EVER
|
|
7
|
+
* reach `Signal.message`, `Signal.metadata` in raw form, or any egress payload:
|
|
8
|
+
* this parser stores ONLY a {@link redactSecret} preview of `Secret` and drops
|
|
9
|
+
* `Match` entirely. The secret-egress negative test proves no raw substring
|
|
10
|
+
* survives.
|
|
11
|
+
*
|
|
12
|
+
* Stock gitleaks emits NO severity (every secret is treated as `high`); the
|
|
13
|
+
* native-severity slot records `null` so a downstream reader knows the four-bucket
|
|
14
|
+
* `high` is the adapter's constant map, not a scanner label.
|
|
15
|
+
*
|
|
16
|
+
* Pure: defensive JSON navigation (never throws on malformed input → `[]`).
|
|
17
|
+
*/
|
|
18
|
+
import { createSignal } from '@opensip-cli/core';
|
|
19
|
+
import { asArray, asObject, getNumber, getString, redactSecret, withNativeSeverity, } from '@opensip-cli/external-tool-adapter';
|
|
20
|
+
/** Read the parsed finding array from the descriptor payload, defensively. */
|
|
21
|
+
function findingArray(raw) {
|
|
22
|
+
// The run loop pre-parses JSON for `kind: 'json'`; fall back to the raw bytes
|
|
23
|
+
// (the acceptance-harness path) so the parser is total either way.
|
|
24
|
+
const json = raw.json ?? safeParse(raw.raw);
|
|
25
|
+
return asArray(json) ?? [];
|
|
26
|
+
}
|
|
27
|
+
function safeParse(text) {
|
|
28
|
+
try {
|
|
29
|
+
return JSON.parse(text);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Normalize one gitleaks finding to a {@link Signal}. Returns `undefined` for a
|
|
37
|
+
* non-object entry so a malformed element is skipped rather than throwing.
|
|
38
|
+
*/
|
|
39
|
+
function normalizeFinding(entry) {
|
|
40
|
+
const finding = asObject(entry);
|
|
41
|
+
if (finding === undefined)
|
|
42
|
+
return undefined;
|
|
43
|
+
const ruleId = getString(finding, 'RuleID') ?? 'gitleaks-secret';
|
|
44
|
+
const message = getString(finding, 'Description') ?? `Secret detected (${ruleId})`;
|
|
45
|
+
const file = getString(finding, 'File') ?? '';
|
|
46
|
+
const line = getNumber(finding, 'StartLine');
|
|
47
|
+
const column = getNumber(finding, 'StartColumn');
|
|
48
|
+
// SECRET HYGIENE: `Secret` is masked to a short non-reversible preview; `Match`
|
|
49
|
+
// (which embeds the secret) is NEVER read into the bag. The metadata carries no
|
|
50
|
+
// raw credential bytes.
|
|
51
|
+
const secretPreview = redactSecret(getString(finding, 'Secret'));
|
|
52
|
+
const metadata = withNativeSeverity({
|
|
53
|
+
nativeFingerprint: getString(finding, 'Fingerprint') ?? null,
|
|
54
|
+
entropy: getNumber(finding, 'Entropy') ?? null,
|
|
55
|
+
tags: asArray(finding.Tags) ?? [],
|
|
56
|
+
...(secretPreview.length > 0 ? { secretPreview } : {}),
|
|
57
|
+
},
|
|
58
|
+
// Stock gitleaks emits no severity — record `null` beside the constant `high`.
|
|
59
|
+
null);
|
|
60
|
+
return createSignal({
|
|
61
|
+
source: 'gitleaks',
|
|
62
|
+
category: 'security',
|
|
63
|
+
severity: 'high',
|
|
64
|
+
ruleId,
|
|
65
|
+
message,
|
|
66
|
+
code: {
|
|
67
|
+
file,
|
|
68
|
+
...(line === undefined ? {} : { line }),
|
|
69
|
+
...(column === undefined ? {} : { column }),
|
|
70
|
+
},
|
|
71
|
+
metadata,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Parse gitleaks JSON output into normalized signals. A clean run (`[]`) yields
|
|
76
|
+
* no findings; every secret maps to a `high`-severity `security` signal with a
|
|
77
|
+
* REDACTED secret preview and no raw credential anywhere.
|
|
78
|
+
*/
|
|
79
|
+
export function parseGitleaksJson(raw, _ctx) {
|
|
80
|
+
const signals = [];
|
|
81
|
+
for (const entry of findingArray(raw)) {
|
|
82
|
+
const signal = normalizeFinding(entry);
|
|
83
|
+
if (signal !== undefined)
|
|
84
|
+
signals.push(signal);
|
|
85
|
+
}
|
|
86
|
+
return signals;
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=parse-gitleaks-json.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parse-gitleaks-json.js","sourceRoot":"","sources":["../src/parse-gitleaks-json.ts"],"names":[],"mappings":"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"}
|