security: use argv for host-side helper calls - #2476
Conversation
📝 WalkthroughWalkthroughReplaces shell-string command invocations with argv-array command calls across onboarding and preflight modules, adds a Changes
Sequence Diagram(s)(omitted) Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
src/lib/onboard.ts (1)
2598-2609: Extract gateway-volume cleanup into a shared helper.This argv-based cleanup looks good, but the orphaned-container path later in the same file still carries a separate shell-pipeline implementation for the same volume removal. Keeping both versions around makes the security fix easy to miss the next time this logic changes.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lib/onboard.ts` around lines 2598 - 2609, Extract the duplicated Docker volume cleanup into a shared helper (e.g., create a function removeGatewayVolumes(gatewayName: string)) that uses runCapture(["docker","volume","ls","-q","--filter", `name=openshell-cluster-${gatewayName}`], {ignoreError:true}) to compute volumeIds, then calls run(["docker","volume","rm", ...volumeIds], {ignoreError:true, suppressOutput:true}) if any exist; replace the inline logic that uses runCapture/run at the volume cleanup site and the separate orphaned-container pipeline that performs the same removal to call removeGatewayVolumes(GATEWAY_NAME) instead so both paths share one implementation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/lib/preflight.ts`:
- Around line 715-724: The current grep call in existingFstabEntry uses a plain
substring search which can match commented lines or unrelated paths; update the
check so it only matches active (non-commented) fstab entries for /swapfile by
changing the command invoked by run([...]) to use an anchored regex (e.g., grep
-qE with a pattern like '^[[:space:]]*[^#].*/swapfile\b') so only uncommented
lines containing /swapfile are detected; keep the subsequent runCapture([...
"sudo", "tee", "-a", "/etc/fstab"]) logic unchanged so the append happens only
when no active entry exists.
In `@test/argv-callers.test.ts`:
- Line 1: Add the required SPDX header lines to the top of this test file (above
the existing import { afterEach, describe, expect, it, vi } from "vitest";
line): insert the exact comment lines "// SPDX-FileCopyrightText: Copyright (c)
2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved." and "//
SPDX-License-Identifier: Apache-2.0" as the first two lines of the file.
- Around line 3-123: This test uses CommonJS require/require.cache which breaks
ESM test runner—replace require.resolve/require/require.cache usage with ESM
dynamic imports and Vitest module-reset/mocking: use string module specifiers
(e.g. const preflightPath = "../dist/lib/preflight.js") and await
import(preflightPath) to load assessHost, probeContainerDns,
getDockerBridgeGatewayIp, and onboard.getGatewayClusterContainerState, call
vi.resetModules() (or vi.restoreAllMocks() + vi.resetModules()) in afterEach
instead of deleting require.cache, and replace manual cache injection for runner
(where you override runCapture) with vi.mock(runnerPath, async () => ({
...(await vi.importActual(runnerPath)), runCapture: vi.fn(() => "running
healthy\n") })) so the tests use Vitest’s ESM-aware mocking and module
lifecycle.
---
Nitpick comments:
In `@src/lib/onboard.ts`:
- Around line 2598-2609: Extract the duplicated Docker volume cleanup into a
shared helper (e.g., create a function removeGatewayVolumes(gatewayName:
string)) that uses runCapture(["docker","volume","ls","-q","--filter",
`name=openshell-cluster-${gatewayName}`], {ignoreError:true}) to compute
volumeIds, then calls run(["docker","volume","rm", ...volumeIds],
{ignoreError:true, suppressOutput:true}) if any exist; replace the inline logic
that uses runCapture/run at the volume cleanup site and the separate
orphaned-container pipeline that performs the same removal to call
removeGatewayVolumes(GATEWAY_NAME) instead so both paths share one
implementation.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Enterprise
Run ID: 4cd66351-0ab1-47ac-9975-34f03d2f7bbc
📒 Files selected for processing (3)
src/lib/onboard.tssrc/lib/preflight.tstest/argv-callers.test.ts
| const existingFstabEntry = | ||
| run(["grep", "-q", "/swapfile", "/etc/fstab"], { | ||
| ignoreError: true, | ||
| suppressOutput: true, | ||
| }).status === 0; | ||
| if (!existingFstabEntry) { | ||
| runCapture(["sudo", "tee", "-a", "/etc/fstab"], { | ||
| ignoreError: false, | ||
| input: "/swapfile none swap sw 0 0\n", | ||
| }); |
There was a problem hiding this comment.
Match only active /swapfile entries in /etc/fstab.
Line 716 uses a plain substring grep, so a commented entry or an unrelated path containing /swapfile will suppress the append. The swap file still works for the current boot, but it will not be re-enabled after reboot.
Suggested fix
const existingFstabEntry =
- run(["grep", "-q", "/swapfile", "/etc/fstab"], {
+ run(["grep", "-qE", "^[[:space:]]*/swapfile[[:space:]]", "/etc/fstab"], {
ignoreError: true,
suppressOutput: true,
}).status === 0;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const existingFstabEntry = | |
| run(["grep", "-q", "/swapfile", "/etc/fstab"], { | |
| ignoreError: true, | |
| suppressOutput: true, | |
| }).status === 0; | |
| if (!existingFstabEntry) { | |
| runCapture(["sudo", "tee", "-a", "/etc/fstab"], { | |
| ignoreError: false, | |
| input: "/swapfile none swap sw 0 0\n", | |
| }); | |
| const existingFstabEntry = | |
| run(["grep", "-qE", "^[[:space:]]*/swapfile[[:space:]]", "/etc/fstab"], { | |
| ignoreError: true, | |
| suppressOutput: true, | |
| }).status === 0; | |
| if (!existingFstabEntry) { | |
| runCapture(["sudo", "tee", "-a", "/etc/fstab"], { | |
| ignoreError: false, | |
| input: "/swapfile none swap sw 0 0\n", | |
| }); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/lib/preflight.ts` around lines 715 - 724, The current grep call in
existingFstabEntry uses a plain substring search which can match commented lines
or unrelated paths; update the check so it only matches active (non-commented)
fstab entries for /swapfile by changing the command invoked by run([...]) to use
an anchored regex (e.g., grep -qE with a pattern like
'^[[:space:]]*[^#].*/swapfile\b') so only uncommented lines containing /swapfile
are detected; keep the subsequent runCapture([... "sudo", "tee", "-a",
"/etc/fstab"]) logic unchanged so the append happens only when no active entry
exists.
| @@ -0,0 +1,123 @@ | |||
| import { afterEach, describe, expect, it, vi } from "vitest"; | |||
There was a problem hiding this comment.
Add the required SPDX header to this new test file.
This file is missing the repository-mandated license header. As per coding guidelines, **/*.{js,mjs,ts,tsx,sh,md}: Every source file must include an SPDX license header: // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. and // SPDX-License-Identifier: Apache-2.0.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@test/argv-callers.test.ts` at line 1, Add the required SPDX header lines to
the top of this test file (above the existing import { afterEach, describe,
expect, it, vi } from "vitest"; line): insert the exact comment lines "//
SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All
rights reserved." and "// SPDX-License-Identifier: Apache-2.0" as the first two
lines of the file.
2f20e7d to
69dc6a9
Compare
There was a problem hiding this comment.
♻️ Duplicate comments (3)
src/lib/preflight.ts (1)
715-725:⚠️ Potential issue | 🟡 MinorTighten the
/swapfilematch before skipping the append.Line 716 still uses a plain substring grep, so a commented entry or an unrelated path containing
/swapfilewill makeexistingFstabEntrylook true. In that case the swapfile works for the current boot, but it will not persist across reboot.Suggested fix
const existingFstabEntry = - run(["grep", "-q", "/swapfile", "/etc/fstab"], { + run(["grep", "-qE", "^[[:space:]]*/swapfile[[:space:]]", "/etc/fstab"], { ignoreError: true, suppressOutput: true, }).status === 0;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lib/preflight.ts` around lines 715 - 725, The current check using run(["grep", "-q", "/swapfile", "/etc/fstab"]) can be fooled by commented lines or other paths containing "/swapfile"; change the existence test (the existingFstabEntry assignment) to search for a non-commented fstab entry that lists /swapfile as the swap file (e.g., a regex like '^[[:space:]]*[^#].*\s+/swapfile(\s|$)' or equivalent) so only active fstab entries match, then keep the append logic using runCapture(["sudo","tee","-a","/etc/fstab"]) unchanged; update the grep invocation inside run(...) that sets existingFstabEntry (and any related variable) to use the stricter pattern.test/argv-callers.test.ts (2)
3-123:⚠️ Potential issue | 🟠 MajorConvert this suite from CommonJS loading/cache mutation to ESM + Vitest-native module control.
Lines 3-123 use
require.resolve,require, andrequire.cache, which violates thetest/ESM rule and makes module lifecycle handling brittle. Useawait import(...),vi.resetModules(), andvi.mock(...)instead.Refactor sketch (ESM-compatible)
-const preflightPath = require.resolve("../dist/lib/preflight.js"); -const onboardPath = require.resolve("../dist/lib/onboard.js"); -const runnerPath = require.resolve("../dist/lib/runner.js"); +const preflightPath = "../dist/lib/preflight.js"; +const onboardPath = "../dist/lib/onboard.js"; +const runnerPath = "../dist/lib/runner.js"; afterEach(() => { vi.restoreAllMocks(); - delete require.cache[preflightPath]; - delete require.cache[onboardPath]; - delete require.cache[runnerPath]; + vi.resetModules(); }); -it("assessHost uses argv commands for host probes", () => { +it("assessHost uses argv commands for host probes", async () => { const seen: Array<string | readonly string[]> = []; - delete require.cache[preflightPath]; - const preflight = require(preflightPath); + const preflight = await import(preflightPath); ... }); -it("probeContainerDns defaults to argv docker run", () => { +it("probeContainerDns defaults to argv docker run", async () => { const seen: Array<string | readonly string[]> = []; - delete require.cache[preflightPath]; - const { probeContainerDns } = require(preflightPath); + const { probeContainerDns } = await import(preflightPath); ... }); -it("getDockerBridgeGatewayIp uses argv docker inspect", () => { - delete require.cache[preflightPath]; - const { getDockerBridgeGatewayIp } = require(preflightPath); +it("getDockerBridgeGatewayIp uses argv docker inspect", async () => { + const { getDockerBridgeGatewayIp } = await import(preflightPath); ... }); -it("getGatewayClusterContainerState uses argv docker inspect", () => { - const actualRunner = require(runnerPath); +it("getGatewayClusterContainerState uses argv docker inspect", async () => { const runCapture = vi.fn(() => "running healthy\n"); - require.cache[runnerPath] = { ... } as any; + vi.mock(runnerPath, async () => { + const actualRunner = await vi.importActual<Record<string, unknown>>(runnerPath); + return { ...actualRunner, runCapture }; + }); - delete require.cache[onboardPath]; - const onboard = require(onboardPath); + const onboard = await import(onboardPath); const state = onboard.getGatewayClusterContainerState(); ... });#!/bin/bash # Verify CommonJS-only patterns are still present in this test file. rg -n -C2 'require\.resolve\(|\brequire\(|require\.cache' test/argv-callers.test.tsAs per coding guidelines,
test/**/*.{js,ts}: test/ directory must use ESM (import/export) for test filesandtest/**/*.test.{js,ts}: Root-level integration tests in test/ directory should use ESM imports and mock external dependencies without calling real NVIDIA APIs.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@test/argv-callers.test.ts` around lines 3 - 123, The tests use CommonJS require/require.cache and require.resolve (preflightPath/onboardPath/runnerPath) which violates ESM test rules; replace all require.resolve/require/require.cache mutations with dynamic ESM imports (await import(...)) and use Vitest module helpers: call vi.resetModules() in afterEach, replace manual cache stubs with vi.mock(...) to override runner.runCapture when testing getGatewayClusterContainerState, and when importing preflight functions (assessHost, probeContainerDns, getDockerBridgeGatewayIp) import them via await import and pass mocked implementations (runCaptureImpl/readFileImpl) rather than mutating require.cache; ensure probeContainerDns and getDockerBridgeGatewayIp assertions still inspect the array commands and options, and remove any use of delete require.cache and require.resolve.
1-1:⚠️ Potential issue | 🟠 MajorAdd the required SPDX header to this test file.
Line 1 starts directly with imports; the mandatory SPDX copyright and license lines are missing.
Proposed fix
+// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 import { afterEach, describe, expect, it, vi } from "vitest";As per coding guidelines,
**/*.{js,mjs,ts,tsx,sh,md}: Every source file must include an SPDX license header: // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. and // SPDX-License-Identifier: Apache-2.0.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@test/argv-callers.test.ts` at line 1, This file is missing the mandatory SPDX header; add the two required comment lines immediately above the first import statement (the line starting with "import { afterEach, describe, expect, it, vi }"): "// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved." and "// SPDX-License-Identifier: Apache-2.0", ensuring they are the very first lines in the file.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@src/lib/preflight.ts`:
- Around line 715-725: The current check using run(["grep", "-q", "/swapfile",
"/etc/fstab"]) can be fooled by commented lines or other paths containing
"/swapfile"; change the existence test (the existingFstabEntry assignment) to
search for a non-commented fstab entry that lists /swapfile as the swap file
(e.g., a regex like '^[[:space:]]*[^#].*\s+/swapfile(\s|$)' or equivalent) so
only active fstab entries match, then keep the append logic using
runCapture(["sudo","tee","-a","/etc/fstab"]) unchanged; update the grep
invocation inside run(...) that sets existingFstabEntry (and any related
variable) to use the stricter pattern.
In `@test/argv-callers.test.ts`:
- Around line 3-123: The tests use CommonJS require/require.cache and
require.resolve (preflightPath/onboardPath/runnerPath) which violates ESM test
rules; replace all require.resolve/require/require.cache mutations with dynamic
ESM imports (await import(...)) and use Vitest module helpers: call
vi.resetModules() in afterEach, replace manual cache stubs with vi.mock(...) to
override runner.runCapture when testing getGatewayClusterContainerState, and
when importing preflight functions (assessHost, probeContainerDns,
getDockerBridgeGatewayIp) import them via await import and pass mocked
implementations (runCaptureImpl/readFileImpl) rather than mutating
require.cache; ensure probeContainerDns and getDockerBridgeGatewayIp assertions
still inspect the array commands and options, and remove any use of delete
require.cache and require.resolve.
- Line 1: This file is missing the mandatory SPDX header; add the two required
comment lines immediately above the first import statement (the line starting
with "import { afterEach, describe, expect, it, vi }"): "//
SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All
rights reserved." and "// SPDX-License-Identifier: Apache-2.0", ensuring they
are the very first lines in the file.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Enterprise
Run ID: 16eda3cd-80f5-4977-ad92-1b99f1e7516d
📒 Files selected for processing (3)
src/lib/onboard.tssrc/lib/preflight.tstest/argv-callers.test.ts
|
✨ Thanks for submitting this pull request that proposes a way to improve security by using argv for host-side helper calls, specifically converting shell-string helper calls to argv-style execution where the command does not actually need a shell. Related open issues: |
|
Correcting my previous malformed comment due to shell quoting. Addressed the ESM test feedback in
Also re-ran |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
test/argv-callers.test.ts (1)
36-53: Tighten the stub fallback so unexpected commands fail fast.At Line 52, the fallback returns
""in both branches, so new/unexpected probes can pass silently and weaken this regression test.Suggested refactor
runCaptureImpl: ( command: string | readonly string[], options?: { ignoreError?: boolean }, ) => { seen.push(command); const key = Array.isArray(command) ? command.join(" ") : command; @@ - return options?.ignoreError ? "" : ""; + if (options?.ignoreError) return ""; + throw new Error(`unexpected command in test stub: ${key}`); },🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@test/argv-callers.test.ts` around lines 36 - 53, The stubbed runCaptureImpl currently returns an empty string for both branches of the fallback, so unexpected commands slip through; change the fallback in runCaptureImpl to return an empty string only when options?.ignoreError is true and otherwise throw an Error (or otherwise fail) with the unexpected command (use the key variable) so tests fail fast when a new/unexpected probe is invoked; reference runCaptureImpl, seen, key and options?.ignoreError to locate and implement this behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@test/argv-callers.test.ts`:
- Around line 36-53: The stubbed runCaptureImpl currently returns an empty
string for both branches of the fallback, so unexpected commands slip through;
change the fallback in runCaptureImpl to return an empty string only when
options?.ignoreError is true and otherwise throw an Error (or otherwise fail)
with the unexpected command (use the key variable) so tests fail fast when a
new/unexpected probe is invoked; reference runCaptureImpl, seen, key and
options?.ignoreError to locate and implement this behavior.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Enterprise
Run ID: af50b030-3d7a-4c0a-96d0-992de3ebf751
📒 Files selected for processing (1)
test/argv-callers.test.ts
There was a problem hiding this comment.
♻️ Duplicate comments (1)
test/argv-callers.test.ts (1)
1-2:⚠️ Potential issue | 🟠 MajorAdd the required SPDX license header at the top of this file.
This new
*.tssource file is missing the mandatory SPDX header.Proposed fix
+// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 import fs from "node:fs"; import { afterEach, describe, expect, it, vi } from "vitest";As per coding guidelines:
**/*.{js,mjs,ts,tsx,sh,md}: Every source file must include an SPDX license header.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@test/argv-callers.test.ts` around lines 1 - 2, Add the required SPDX license header as the very first line of the file (above any imports like the existing "import fs from 'node:fs';" and "import { afterEach, describe, expect, it, vi } from 'vitest';") — e.g. add a single-line comment such as "// SPDX-License-Identifier: MIT" (or the project's approved identifier) to satisfy the repository's linting/license rule.
🧹 Nitpick comments (1)
test/argv-callers.test.ts (1)
101-108: Also assertignoreError: truein the gateway inspect callback test.This test currently validates argv shape but not the options object. Adding the option assertion will better lock the intended contract.
Proposed hardening
- const gateway = getDockerBridgeGatewayIp((command: string | readonly string[]) => { + const gateway = getDockerBridgeGatewayIp(( + command: string | readonly string[], + opts?: { ignoreError?: boolean }, + ) => { seen.push(command); + expect(opts?.ignoreError).toBe(true); return "172.17.0.1fd00:abcd::1\n"; });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@test/argv-callers.test.ts` around lines 101 - 108, The test should also assert that the options object includes ignoreError: true when getDockerBridgeGatewayIp invokes the docker inspect callback; update the test's mock callback signature (the function passed into getDockerBridgeGatewayIp) to accept (command, options), assert/options.ignoreError === true (or deep-equal { ignoreError: true } depending on how options is structured), and still push the command to seen; ensure you reference the existing getDockerBridgeGatewayIp test callback so the assertion is added within that mock rather than a separate check.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@test/argv-callers.test.ts`:
- Around line 1-2: Add the required SPDX license header as the very first line
of the file (above any imports like the existing "import fs from 'node:fs';" and
"import { afterEach, describe, expect, it, vi } from 'vitest';") — e.g. add a
single-line comment such as "// SPDX-License-Identifier: MIT" (or the project's
approved identifier) to satisfy the repository's linting/license rule.
---
Nitpick comments:
In `@test/argv-callers.test.ts`:
- Around line 101-108: The test should also assert that the options object
includes ignoreError: true when getDockerBridgeGatewayIp invokes the docker
inspect callback; update the test's mock callback signature (the function passed
into getDockerBridgeGatewayIp) to accept (command, options),
assert/options.ignoreError === true (or deep-equal { ignoreError: true }
depending on how options is structured), and still push the command to seen;
ensure you reference the existing getDockerBridgeGatewayIp test callback so the
assertion is added within that mock rather than a separate check.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Enterprise
Run ID: 0d564ec9-f142-4f3e-84e0-58b990170544
📒 Files selected for processing (1)
test/argv-callers.test.ts
|
Thanks for the contribution here. I am marking this PR as superseded rather than merging it, because later merged work covered the same security objective at a broader layer. Relevant merged PRs:
Those changes mean current main rejects shell strings for run/runInteractive/runCapture and rejects shell: true on argv helper calls, with intentional shell behavior isolated behind explicit runShell usage. Closing this as superseded. |
Summary
This is a small Phase 1 pass on #1889.
It converts shell-string helper calls to argv-style execution where the command does not actually need a shell, while intentionally leaving explicit shell-dependent cases in place, such as:
What changed
src/lib/preflight.tsto argv formsrc/lib/onboard.tsto argv formValidation
npm run build:clinpm test -- test/argv-callers.test.tsNotes
I also checked the broader test suite locally, but there are existing unrelated failures/noise on current main, so I kept validation scoped to the touched paths for this PR.
Summary by CodeRabbit
Bug Fixes
Tests