@codacy/tools-opengrep-1 0.11.0 → 0.12.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.
Files changed (3) hide show
  1. package/README.md +3 -0
  2. package/dist/index.js +106 -77
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -126,3 +126,6 @@ curl -fsSL https://raw.githubusercontent.com/opengrep/opengrep/main/install.sh |
126
126
  - **Opengrep errors**: Parse errors (syntax errors, partial parsing) from Opengrep's JSON `errors` array are converted to structured `AnalysisError` objects with `filePath`. Stderr (scan status table) is logged at debug level only.
127
127
  - **Rule ID format**: Semgrep-rules use `{dir}.{filename}.{ruleId}` (dots), GitLab rules use `{dir}_{filename}` (underscores), Codacy custom rules keep their original IDs.
128
128
  - **Install / binary download**: `install()` downloads the bare per-platform binary from `https://github.com/opengrep/opengrep/releases/download/v{version}/{asset}` and `chmod +x`es it into `~/.codacy/tools/`. The release tag carries a `v` prefix (`v1.16.4`) even though `preferredVersion` does not. On Linux the libc flavour is detected with an `ldd /bin/sh` probe (`musl` → musllinux, else manylinux); detection failures fall back to glibc/manylinux. Unlike install.sh, the Codacy-managed install does **not** verify the cosign `.cert`/`.sig` signatures — if signature verification becomes a requirement, fetch those sibling assets and validate before chmod.
129
+ - **Argv-size chunking (no directory walk)**: The adapter always passes Opengrep an **explicit file list** — the runner's already git-ignored, exclude-filtered target set — so Opengrep never walks the repository tree itself. `chunkFilesByArgvBudget()` splits that list into the **fewest, largest** chunks that each fit `ARGV_BYTE_BUDGET` (200 KB, well under `ARG_MAX`), and each chunk is scanned in its own `opengrep scan` invocation; findings and errors are merged across chunks. Chunk by **size, not count**: every invocation re-compiles `rules.yaml`, so fewer/larger chunks amortize that fixed cost. The earlier directory/“root” fallback was removed because on huge repos (a single tracked file at the repo root collapsed the minimal-cover dirs to `.`) it made Opengrep walk the entire tree — re-scanning excluded/generated/vendored files and timing out at the runner's 10-minute limit.
130
+ - **File-size cap**: invocations pass `--max-target-bytes 1000000` (~1 MB). This restores Opengrep's default size cap — the adapter previously passed `0`, which **disables** the cap, so giant generated/minified bundles were scanned and dominated runtime. The cap applies to explicitly-listed files (verified on v1.16.4). Note: `--exclude-minified-files` is **not** used — that flag is rejected by the bundled binary unless `--experimental` (a different engine) is also passed, so we rely on the size cap instead. (The runner also enforces a tighter 150 KB per-file limit centrally before files reach the adapter, so this `--max-target-bytes` is now a secondary defense.)
131
+ - **Partial results on timeout**: the per-chunk loop checks `ctx.signal` (the runner's timeout abort) between chunks and passes it to `spawnTool`, so an in-flight scan is killed promptly. On abort the adapter keeps the findings from completed chunks and adds a single `Timeout` error (→ `status: "partial"`) rather than losing everything. This is why chunking matters beyond ARG_MAX: it's the granularity at which a long scan can be stopped with results preserved.
package/dist/index.js CHANGED
@@ -30414,6 +30414,17 @@ var PATTERN_META = new Map(
30414
30414
  }
30415
30415
  ])
30416
30416
  );
30417
+ var MAX_SNIPPET_PREVIEW = 60;
30418
+ var MAX_ERROR_MESSAGE = 200;
30419
+ function summarizeErrorMessage(message) {
30420
+ const collapsed = message.replace(/`[\s\S]*?`/g, (snippet) => {
30421
+ const inner = snippet.slice(1, -1).replace(/\s+/g, " ").trim();
30422
+ const preview = inner.length > MAX_SNIPPET_PREVIEW ? `${inner.slice(0, MAX_SNIPPET_PREVIEW)}\u2026` : inner;
30423
+ return `\`${preview}\``;
30424
+ });
30425
+ const oneLine = collapsed.replace(/\s+/g, " ").trim();
30426
+ return oneLine.length > MAX_ERROR_MESSAGE ? `${oneLine.slice(0, MAX_ERROR_MESSAGE)}\u2026` : oneLine;
30427
+ }
30417
30428
  function ruleMetadata(ruleId) {
30418
30429
  const patternId = `${TOOL_ID}_${ruleId}`;
30419
30430
  return PATTERN_META.get(patternId) ?? { category: "Security", severity: "Info" };
@@ -30480,46 +30491,75 @@ function truncate(str, maxLen) {
30480
30491
  return str.slice(0, maxLen - 3) + "...";
30481
30492
  }
30482
30493
  var ARGV_BYTE_BUDGET = 200 * 1024;
30483
- function minimalCoverDirs(files) {
30484
- const dirs = /* @__PURE__ */ new Set();
30485
- for (const f of files) {
30486
- const d = import_path.default.dirname(f);
30487
- dirs.add(d === "" ? "." : d);
30488
- }
30489
- const sorted = [...dirs].sort((a, b) => {
30490
- if (a === ".") return -1;
30491
- if (b === ".") return 1;
30492
- return a.split(import_path.default.sep).length - b.split(import_path.default.sep).length;
30493
- });
30494
- const kept = [];
30495
- for (const d of sorted) {
30496
- const covered = kept.some((k) => {
30497
- if (k === d) return true;
30498
- if (k === ".") return true;
30499
- return d.startsWith(k + import_path.default.sep);
30500
- });
30501
- if (!covered) kept.push(d);
30494
+ function chunkFilesByArgvBudget(files) {
30495
+ const chunks = [];
30496
+ let current = [];
30497
+ let currentBytes = 0;
30498
+ for (const file of files) {
30499
+ const cost = Buffer.byteLength(file) + 1;
30500
+ if (current.length > 0 && currentBytes + cost > ARGV_BYTE_BUDGET) {
30501
+ chunks.push(current);
30502
+ current = [];
30503
+ currentBytes = 0;
30504
+ }
30505
+ current.push(file);
30506
+ currentBytes += cost;
30502
30507
  }
30503
- return kept;
30508
+ if (current.length > 0) chunks.push(current);
30509
+ return chunks;
30504
30510
  }
30505
- function argvBytes(paths) {
30506
- let total = 0;
30507
- for (const p of paths) total += Buffer.byteLength(p) + 1;
30508
- return total;
30509
- }
30510
- function chooseScanTargets(validatedAbsoluteFiles, repositoryRoot) {
30511
- if (argvBytes(validatedAbsoluteFiles) <= ARGV_BYTE_BUDGET) {
30512
- return { targets: validatedAbsoluteFiles, strategy: "files" };
30513
- }
30514
- const relFiles = validatedAbsoluteFiles.map((f) => import_path.default.relative(repositoryRoot, f));
30515
- const dirs = minimalCoverDirs(relFiles);
30516
- if (dirs.length > 0 && !(dirs.length === 1 && dirs[0] === ".")) {
30517
- const absoluteDirs = dirs.map((d) => import_path.default.resolve(repositoryRoot, d));
30518
- if (argvBytes(absoluteDirs) <= ARGV_BYTE_BUDGET) {
30519
- return { targets: absoluteDirs, strategy: "dirs" };
30511
+ async function scanChunk(binary, args, ctx, errors) {
30512
+ try {
30513
+ const { stdout, stderr, exitCode } = await (0, import_tooling.spawnTool)(binary, args, {
30514
+ cwd: ctx.repositoryRoot,
30515
+ logger: ctx.logger,
30516
+ // Killed promptly if the runner's timeout aborts mid-scan (see the loop
30517
+ // in analyze() which then returns the chunks completed so far).
30518
+ signal: ctx.signal
30519
+ });
30520
+ if (stderr.trim()) {
30521
+ ctx.logger.debug(`opengrep stderr:
30522
+ ${stderr.trim()}`);
30520
30523
  }
30524
+ if (!stdout.trim()) {
30525
+ if (exitCode >= 2) {
30526
+ errors.push({
30527
+ toolId: TOOL_ID,
30528
+ phase: "toolInvoke",
30529
+ kind: "NonZeroExit",
30530
+ message: `opengrep exited with code ${exitCode}${stderr ? `: ${stderr.trim()}` : ""}`,
30531
+ level: "error"
30532
+ });
30533
+ }
30534
+ return { results: [], errors: [] };
30535
+ }
30536
+ let output;
30537
+ try {
30538
+ output = JSON.parse(stdout);
30539
+ } catch (err) {
30540
+ errors.push({
30541
+ toolId: TOOL_ID,
30542
+ phase: "outputParse",
30543
+ kind: "MalformedOutput",
30544
+ message: `Failed to parse opengrep JSON output: ${err instanceof Error ? err.message : String(err)}`,
30545
+ level: "error"
30546
+ });
30547
+ return { results: [], errors: [] };
30548
+ }
30549
+ return { results: output.results ?? [], errors: output.errors ?? [] };
30550
+ } catch (err) {
30551
+ if (ctx.signal?.aborted) {
30552
+ return { results: [], errors: [] };
30553
+ }
30554
+ errors.push({
30555
+ toolId: TOOL_ID,
30556
+ phase: "toolInvoke",
30557
+ kind: "InvocationError",
30558
+ message: `Failed to run opengrep: ${err instanceof Error ? err.message : String(err)}`,
30559
+ level: "error"
30560
+ });
30561
+ return { results: [], errors: [] };
30521
30562
  }
30522
- return { targets: [repositoryRoot], strategy: "root" };
30523
30563
  }
30524
30564
  async function materializeConfig(ctx, enabledPatterns) {
30525
30565
  let rulesContent;
@@ -30750,17 +30790,13 @@ var adapter = {
30750
30790
  if (targetPathSet.size === 0) {
30751
30791
  return (0, import_tooling.buildResult)(TOOL_ID, startedAt, /* @__PURE__ */ new Date(), issues, errors, 0);
30752
30792
  }
30753
- const { targets: scanTargets, strategy: scanStrategy } = chooseScanTargets(
30754
- validatedAbsoluteFiles,
30755
- ctx.repositoryRoot
30756
- );
30757
- ctx.logger.debug("Opengrep scan targets chosen", {
30793
+ const fileChunks = chunkFilesByArgvBudget(validatedAbsoluteFiles);
30794
+ ctx.logger.debug("Opengrep scan chunks computed", {
30758
30795
  toolId: TOOL_ID,
30759
- strategy: scanStrategy,
30760
- targetCount: scanTargets.length,
30796
+ chunkCount: fileChunks.length,
30761
30797
  fileCount: ctx.targetFiles.length
30762
30798
  });
30763
- const args = [
30799
+ const baseArgs = [
30764
30800
  "scan",
30765
30801
  "--json",
30766
30802
  "--config",
@@ -30770,44 +30806,40 @@ var adapter = {
30770
30806
  "--timeout-threshold",
30771
30807
  "50",
30772
30808
  "--max-target-bytes",
30773
- "0",
30809
+ "1000000",
30774
30810
  "--max-memory",
30775
30811
  "5000",
30776
- "--disable-version-check",
30777
- ...scanTargets
30812
+ "--disable-version-check"
30778
30813
  ];
30779
30814
  try {
30780
- const { stdout, stderr, exitCode } = await (0, import_tooling.spawnTool)(binary, args, {
30781
- cwd: ctx.repositoryRoot,
30782
- logger: ctx.logger
30783
- });
30784
- if (!stdout.trim()) {
30785
- if (exitCode >= 2) {
30786
- errors.push({
30787
- toolId: TOOL_ID,
30788
- phase: "toolInvoke",
30789
- kind: "NonZeroExit",
30790
- message: `opengrep exited with code ${exitCode}${stderr ? `: ${stderr.trim()}` : ""}`,
30791
- level: "error"
30792
- });
30793
- }
30794
- return (0, import_tooling.buildResult)(TOOL_ID, startedAt, /* @__PURE__ */ new Date(), issues, errors, ctx.targetFiles.length);
30815
+ const allFindings = [];
30816
+ const allOgErrors = [];
30817
+ let scannedCount = 0;
30818
+ for (const chunk of fileChunks) {
30819
+ if (ctx.signal?.aborted) break;
30820
+ const chunkOutput = await scanChunk(binary, [...baseArgs, ...chunk], ctx, errors);
30821
+ allFindings.push(...chunkOutput.results);
30822
+ allOgErrors.push(...chunkOutput.errors);
30823
+ if (ctx.signal?.aborted) break;
30824
+ scannedCount += chunk.length;
30825
+ ctx.onProgress?.(`Scanning files ${scannedCount}/${validatedAbsoluteFiles.length}`);
30795
30826
  }
30796
- let output;
30797
- try {
30798
- output = JSON.parse(stdout);
30799
- } catch (err) {
30827
+ if (ctx.signal?.aborted) {
30800
30828
  errors.push({
30801
30829
  toolId: TOOL_ID,
30802
- phase: "outputParse",
30803
- kind: "MalformedOutput",
30804
- message: `Failed to parse opengrep JSON output: ${err instanceof Error ? err.message : String(err)}`,
30830
+ phase: "toolInvoke",
30831
+ kind: "Timeout",
30832
+ message: `opengrep timed out; returning partial results for ${scannedCount}/${validatedAbsoluteFiles.length} files`,
30805
30833
  level: "error"
30806
30834
  });
30807
- return (0, import_tooling.buildResult)(TOOL_ID, startedAt, /* @__PURE__ */ new Date(), issues, errors, ctx.targetFiles.length);
30835
+ ctx.logger.warn("opengrep timed out; returning partial results", {
30836
+ toolId: TOOL_ID,
30837
+ scannedFiles: scannedCount,
30838
+ totalFiles: validatedAbsoluteFiles.length
30839
+ });
30808
30840
  }
30809
- if (output.errors && output.errors.length > 0) {
30810
- for (const ogError of output.errors) {
30841
+ if (allOgErrors.length > 0) {
30842
+ for (const ogError of allOgErrors) {
30811
30843
  if (ogError.path) {
30812
30844
  const absError = import_path.default.isAbsolute(ogError.path) ? ogError.path : import_path.default.resolve(ctx.repositoryRoot, ogError.path);
30813
30845
  const relError = import_path.default.relative(ctx.repositoryRoot, absError);
@@ -30820,6 +30852,7 @@ var adapter = {
30820
30852
  if (ogError.path && message.includes(ogError.path)) {
30821
30853
  message = message.replace(new RegExp(`at line \\S+:\\d+:\\n\\s*`), "");
30822
30854
  }
30855
+ message = summarizeErrorMessage(message);
30823
30856
  const level = ogError.level === "error" ? "error" : "warning";
30824
30857
  const filePath = ogError.path ? import_path.default.isAbsolute(ogError.path) ? import_path.default.relative(ctx.repositoryRoot, ogError.path) : ogError.path : void 0;
30825
30858
  errors.push({
@@ -30835,7 +30868,7 @@ var adapter = {
30835
30868
  const enabledPatterns = new Set(codacyConfig.patterns.map((p) => p.patternId));
30836
30869
  const configPrefix = configFile ? computeConfigPrefix(configFile, ctx.repositoryRoot) : "";
30837
30870
  const findingsByFile = /* @__PURE__ */ new Map();
30838
- for (const finding of output.results) {
30871
+ for (const finding of allFindings) {
30839
30872
  if (finding.extra.is_ignored) continue;
30840
30873
  const absFinding = import_path.default.isAbsolute(finding.path) ? finding.path : import_path.default.resolve(ctx.repositoryRoot, finding.path);
30841
30874
  const relFinding = import_path.default.relative(ctx.repositoryRoot, absFinding);
@@ -30877,10 +30910,6 @@ var adapter = {
30877
30910
  issues.push(issue);
30878
30911
  }
30879
30912
  }
30880
- if (stderr.trim()) {
30881
- ctx.logger.debug(`opengrep stderr:
30882
- ${stderr.trim()}`);
30883
- }
30884
30913
  } catch (err) {
30885
30914
  errors.push({
30886
30915
  toolId: TOOL_ID,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codacy/tools-opengrep-1",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "Opengrep adapter — CLI-mode multi-language SAST scanner",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -10,7 +10,7 @@
10
10
  ],
11
11
  "dependencies": {
12
12
  "yaml": "^2.6.0",
13
- "@codacy/tooling": "0.11.0"
13
+ "@codacy/tooling": "0.12.0"
14
14
  },
15
15
  "license": "MIT",
16
16
  "author": "Codacy <support@codacy.com> (https://www.codacy.com)",