Skip to content

Comments

chore: audit guardrails and observability (#290)#291

Open
flyingrobots wants to merge 8 commits intomainfrom
chore/audit-guardrails
Open

chore: audit guardrails and observability (#290)#291
flyingrobots wants to merge 8 commits intomainfrom
chore/audit-guardrails

Conversation

@flyingrobots
Copy link
Owner

@flyingrobots flyingrobots commented Feb 24, 2026

Summary

  • Add ESLint 9 flat config with recommended rules, escalate no-unused-vars to error, fix all lint violations
  • Replace execSync with execFileSync in processCommitCmd and add rev-parse --verify SHA validation to eliminate shell/option injection
  • Add lint step and coverage reporting (v8 + lcov) to CI pipeline with npm cache and artifact upload
  • Add @eslint/js, globals, and @vitest/coverage-v8 as explicit devDependencies

Problem Statement

  • Forensic audit (AUDIT_REPORT.md) identified shell injection risk in processCommitCmd where user-supplied SHA was interpolated into execSync shell string
  • No linter enforced in CI — unused imports and useless regex escapes accumulated undetected
  • Coverage tooling configured but never executed or reported in CI — dead letter configuration

Closes #290

ADR Compliance (Required)

Relevant ADR(s)

  • None

Compliance Declaration

  • This PR is fully compliant with all checked ADRs.

Architecture Laws Checklist (Hard Gates)

Canonical Truth & Context

  • Graph remains canonical truth (no dual truth with generated files).
  • No hidden worktree coupling introduced in core/domain/materialization paths.
  • Context-sensitive behavior is explicit (--at, --observer, --trust) or deterministically defaulted.
  • Resolved context is surfaced in output metadata where applicable.

Determinism & Provenance

  • Pure query/materialization paths remain deterministic for identical inputs.
  • Mutations/materializations include provenance receipts/envelopes where required.
  • Cache keys (if used) are derived only from semantic inputs + pinned versions.

Artifact Hygiene

  • No forbidden generated artifact paths are tracked.
  • Any generated artifacts intentionally tracked are in allowlisted publish paths only.
  • Pre-commit/CI policy checks updated or confirmed valid.

Contracts & Compatibility

  • Machine-facing outputs are schema-versioned.
  • Breaking contract changes include version bump + migration notes.
  • Backward compatibility impact is documented below.

Extension/Effects Safety (if applicable)

  • Extension behavior does not bypass capability restrictions.
  • Effectful operations use explicit plan/apply semantics and emit receipts.
  • Timeouts/resource bounds are defined for new script/effect paths.

Scope Control

  • PR is single-purpose/cohesive (no unrelated refactors).
  • Any non-essential refactor is split into separate PR(s) or explicitly justified.

Backward Compatibility

  • CLI/API contract changes: None
  • Data model/storage changes: None
  • Migration required?: No
  • User-facing behavior changes: None — execFileSync produces identical output to execSync for the same git command; rev-parse --verify fails fast on invalid refs that would have failed anyway in git log.

Test Plan (Required)

Unit

  • Added/updated tests for changed logic
  • Commands:
npm test
# 577 tests passing across 29 files

Integration

  • Added/updated integration tests
  • Commands:
npm run lint   # 0 errors, 0 warnings
npm test       # 577 passing

Determinism

  • Determinism assertions included for relevant paths
  • Method: Regex changes are semantically equivalent (unescaped / in character class)
  • Commands:
npm test -- test/remote.test.js test/validators.test.js

Contract/Schema

  • Schema validation updated/passing
  • Commands:
npm test -- test/contracts.integration.test.js

Policy Gates

  • Mechanical architecture gates pass
  • Commands:
# Pre-commit hooks pass on all 6 commits

Security / Trust Impact

  • Threat surface changed?: Yes — reduced. Shell injection vector in processCommitCmd eliminated via execFileSync + SHA validation with rev-parse --verify.
  • Trust policy impact: None
  • Provenance/audit impact: None
  • New failure modes introduced: rev-parse --verify will reject invalid refs earlier (fail-fast, not a new failure — these refs would have failed in git log anyway).

Performance Impact

  • Hot path affected?: No — processCommitCmd is called once per post-commit hook invocation.
  • Expected impact (latency/memory/io): Negligible — one additional git rev-parse call per commit processing.
  • Benchmarks or profiling evidence: N/A — single synchronous git plumbing call.

Observability / Debuggability

  • Errors are actionable and include context.
  • Logs/diagnostics added or updated where needed.
  • git mind status / diagnostics updated if writeback/eventing behavior changed.

Operational Notes

  • Feature flag (if any): None
  • Rollback strategy: Revert PR — no data migration involved
  • Operational caveats: npm ci now installs @vitest/coverage-v8, @eslint/js, and globals as devDependencies

Linked Issues / Milestones

Add ESLint 9 flat config with recommended rules and node globals.
Fix two no-useless-escape errors in regex character classes and
remove a stale eslint-disable directive in lens.js.
Replace shell-interpolated execSync with execFileSync array form to
eliminate potential command injection via user-supplied SHA argument.
Add ESLint step to CI workflow before tests. Configure v8 coverage
provider in vitest and add test:coverage script for local use.
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 24, 2026

Caution

Review failed

An error occurred during the review process. Please try again later.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6cfc2be and 20df34f.

📒 Files selected for processing (4)
  • .github/workflows/ci.yml
  • src/context.js
  • src/suggest.js
  • test/context.test.js

Summary by CodeRabbit

  • Chores

    • Strengthened CI/CD pipeline with integrated linting, test coverage reporting, and automated artifact uploads
    • Added configuration for code quality checks and test coverage collection
  • Tests

    • Expanded validation test suite to ensure safe input handling and command execution

Walkthrough

Replaces unsafe shell-invoking git calls with execFileSync across CLI and core, adds a flat ESLint 9 config and CI lint step, enables Vitest v8 coverage with a coverage npm script and CI artifact upload, simplifies several regex literals and imports, and extends tests for commit-range sanitization.

Changes

Cohort / File(s) Summary
CI Pipeline
.github/workflows/ci.yml
Replaces single test step with lint → vitest run (coverage) → upload coverage artifact; adds Node cache configuration and makes upload run-always with artifact name including Node matrix.
ESLint config & devDeps
eslint.config.js, package.json
Adds flat ESLint config (eslint.config.js) and devDependencies @eslint/js and globals; config sets latest ecmaVersion, module sourceType, Node globals, custom no-unused-vars, and ignored paths.
Test coverage config & scripts
vitest.config.js, package.json
Adds Vitest coverage block (provider: v8, reporter: text+lcov, reportsDirectory ./coverage, include src/**/*.js) and test:coverage npm script; adds @vitest/coverage-v8 devDependency.
Git command hardening
src/context.js, src/cli/commands.js, src/merge.js
Replaces shell-interpolated execSync with execFileSync + arg arrays, adds stricter sanitizeGitArg (reject leading -, whitespace, backslash), adds git-log fallback path and trims command output.
Imports & small refactors
src/cli/commands.js, src/nodes.js, src/suggest.js, src/doctor.js, src/dag.js
Removes unused imports, comments out a future import usage, and renames local bindings (graph_graph, src_src) — no public API changes.
Regex literal simplifications
src/remote.js, src/validators.js
Simplifies escaping inside character classes for CROSS_REPO_ID_REGEX and NODE_ID_REGEX (semantically unchanged).
Lint directive removal
src/lens.js
Removes an eslint-disable comment so a following console.warn is now linted under the new ESLint config.
Tests
test/context.test.js
Adds tests asserting extractCommitContext rejects ranges containing spaces, tabs, backslashes, and leading hyphens.

Sequence Diagram(s)

sequenceDiagram
  participant CLI as CLI command
  participant Node as Node process
  participant Git as Git
  participant FS as Filesystem

  CLI->>Node: call execFileSync('git', ['log', '--pretty=format:%B', <args>])
  Note right of Node: sanitizeGitArg validates args\n(rejects leading '-' and unsafe chars)
  Node->>Git: spawn with argv array
  Git-->>Node: returns stdout/stderr
  Node->>FS: write/read diff / coverage files (as needed)
  Node-->>CLI: return trimmed output / error code
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~27 minutes

Possibly related PRs

Poem

Args scrubbed clean, no shells to misbehave,
Lint now flat, the warnings face the grave,
Coverage packed, artifacts take flight,
Regexes unescaped, patterns look polite,
Safer git calls — the audit sleeps at night.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed Title 'chore: audit guardrails and observability' clearly and specifically describes the primary change: hardening guardrails and adding observability mechanisms post-forensic audit.
Description check ✅ Passed Description comprehensively covers all template sections: Summary (3 bullets), Problem Statement, ADR Compliance, Architecture Laws Checklist, Scope Control, Backward Compatibility, Test Plan with commands, Security/Trust Impact, Performance Impact, Observability/Debuggability, Operational Notes, and Linked Issues.
Linked Issues check ✅ Passed All three coding requirements from issue #290 are fully satisfied: RISK-002 shell-injection mitigation via execFileSync+rev-parse [src/cli/commands.js, src/context.js, src/merge.js]; RISK-003 ESLint 9 flat config enforcement [eslint.config.js, linter step in CI]; RISK-006 coverage execution and reporting in CI [vitest.config.js, CI step, @vitest/coverage-v8 dependency].
Out of Scope Changes check ✅ Passed All changes are tightly scoped to guardrails and observability: unused import removals follow linter escalation; variable renames (_graph, _src) comply with no-unused-vars pattern; regex escaping simplifications ensure consistency; no unrelated refactoring or feature additions detected.
Docstring Coverage ✅ Passed Docstring coverage is 85.71% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch chore/audit-guardrails

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/ci.yml:
- Around line 33-40: The CI workflow currently runs the "Install dependencies"
step with npm ci every run causing redundant downloads; update the job that sets
up Node (actions/setup-node@v4) to enable its built-in npm cache by adding the
cache input (e.g., cache: 'npm') so the "Install dependencies" step using npm ci
re-uses cached node_modules across runs and speeds up subsequent "Run linter"
and "Run tests" steps; locate the actions/setup-node@v4 invocation in the
workflow and add the cache configuration there.
- Around line 36-40: CI currently runs only "npm test" so Vitest's lcov reporter
and test coverage are never produced; update the workflow to invoke the
coverage-enabled test script (e.g., run the existing "test:coverage" npm script
or run "npm test" with the --coverage flag) so Vitest emits the ./coverage lcov
output, then add a step to upload the ./coverage directory (using the
upload-artifact action) and/or publish it to your coverage service; optionally
add coverage.thresholds in vitest.config.js so Vitest can fail the job when
thresholds are not met.
- Around line 36-40: Replace all remaining uses of execSync with safe,
argumentized process calls and input validation: specifically change the git
remote URL retrieval in merge.js (the execSync on line ~19) and each execSync in
context.js (the calls around lines ~89, ~118, ~135) to use
child_process.execFileSync or child_process.spawnSync with the command and an
args array (and set shell: false), and ensure any values interpolated into
arguments are validated or whitelisted rather than concatenated into a shell
string; mirror the approach used in processCommitCmd to pass args as arrays and
avoid shell invocation so no shell-injection vectors remain.

In `@eslint.config.js`:
- Around line 14-16: Update the ESLint rule configuration for no-unused-vars in
the rules object: change its severity from 'warn' to 'error' and add a
varsIgnorePattern to mirror the existing argsIgnorePattern so intentionally
prefixed variables are ignored; specifically update the 'no-unused-vars' entry
to include argsIgnorePattern: '^_' and varsIgnorePattern: '^_' (keeping the
existing options shape) so CI fails on real unused-variable issues while
allowing underscore-prefixed exceptions.
- Around line 1-2: The ESLint imports in eslint.config.js (js from '@eslint/js'
and globals from 'globals') must be explicit devDependencies; update
package.json to add "@eslint/js" and "globals" under devDependencies (pin
versions compatible with your installed eslint v9), then run your package
manager to install them (e.g., npm install --save-dev `@eslint/js` globals or yarn
add -D `@eslint/js` globals) so the imports in eslint.config.js resolve reliably.

In `@package.json`:
- Line 24: Add the missing `@vitest/coverage-v8` devDependency (pin it to the same
major version as the installed vitest) so the test:coverage script can use
provider: 'v8' from vitest.config.js; remove the ad-hoc --exclude flags from the
"test:coverage" script in package.json and instead add the excluded test paths
under coverage.exclude in vitest.config.js so excluded tests don't run but are
properly omitted from coverage instrumentation.

In `@src/cli/commands.js`:
- Around line 298-301: In processCommitCmd, validate the incoming sha before
calling git log to prevent git option injection: run git rev-parse --verify on
the sha (like the pattern used in src/epoch.js) via execFileSync and
reject/throw or return an error if rev-parse fails, then proceed to call
execFileSync('git', ['log', '-1', '--format=%B', sha], ...); this ensures
malformed refs (including ones starting with '-') cannot be interpreted as CLI
options.

In `@vitest.config.js`:
- Around line 10-14: The coverage config is missing coverage.include so
unimported source files are omitted; update the Vitest config object (the
coverage section) to add a coverage.include array that explicitly lists your
source patterns (e.g., 'src/**/*.ts' or similar) so coverage runs include all
intended source files even if not imported during tests, and keep existing keys
provider, reporter, and reportsDirectory unchanged.

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 031ddd0 and c00609d.

📒 Files selected for processing (8)
  • .github/workflows/ci.yml
  • eslint.config.js
  • package.json
  • src/cli/commands.js
  • src/lens.js
  • src/remote.js
  • src/validators.js
  • vitest.config.js
💤 Files with no reviewable changes (1)
  • src/lens.js

"scripts": {
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage --exclude test/contracts.integration.test.js --exclude test/content.test.js --exclude test/version.test.js",
Copy link
Contributor

@coderabbitai coderabbitai bot Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, find and read package.json to verify the devDependencies
find . -name "package.json" -type f -not -path "*/node_modules/*" | head -5

Repository: flyingrobots/git-mind

Length of output: 79


🏁 Script executed:

# Read the package.json to check devDependencies
cat -n package.json | grep -A 10 "devDependencies"

Repository: flyingrobots/git-mind

Length of output: 272


🏁 Script executed:

# Check if `@vitest/coverage-v8` exists in node_modules
ls -la node_modules/@vitest/ 2>/dev/null | head -20 || echo "No `@vitest` directory found or node_modules not available"

Repository: flyingrobots/git-mind

Length of output: 47


🏁 Script executed:

# Check for vitest.config.js or similar config files
find . -name "vitest.config.*" -o -name "vite.config.*" | grep -v node_modules

Repository: flyingrobots/git-mind

Length of output: 83


🏁 Script executed:

# Check the actual test:coverage script line in detail
sed -n '20,30p' package.json

Repository: flyingrobots/git-mind

Length of output: 486


🏁 Script executed:

# Read the vitest.config.js to check coverage configuration
cat -n vitest.config.js

Repository: flyingrobots/git-mind

Length of output: 488


🏁 Script executed:

# Check vitest documentation info - try to see what's in vitest's own config
# and whether coverage is pre-included or requires a separate package
rg "coverage" vitest.config.js

Repository: flyingrobots/git-mind

Length of output: 118


🏁 Script executed:

# Let's also check if there are any test files that might give us hints about coverage setup
ls -la test/ 2>/dev/null | head -20

Repository: flyingrobots/git-mind

Length of output: 1345


🏁 Script executed:

# Double-check: Does --exclude affect coverage instrumentation in vitest?
# Let me search for any documentation or hints in the codebase about this
rg "coverage.exclude" . --type js --type json 2>/dev/null | head -10

Repository: flyingrobots/git-mind

Length of output: 47


🏁 Script executed:

# Verify the full context around test:coverage script to see lines 20-30 properly
sed -n '20,35p' package.json

Repository: flyingrobots/git-mind

Length of output: 585


@vitest/coverage-v8 is missing from devDependencies — the test:coverage script will fail on any clean install.

The vitest.config.js explicitly specifies provider: 'v8' (line 11), which requires its own separate package. The interactive install prompt that Vitest offers does not fire in scripted contexts like npm run or CI/CD pipelines — you must declare the dependency explicitly.

The --exclude flags in the script prevent test files from executing, which incidentally prevents them from contributing to coverage reports (can't instrument code that never runs). However, this is an indirect mechanism. The proper way to exclude files from coverage scope is coverage.exclude in the config, not relying on test execution filters.

🔧 Add the missing package
   "devDependencies": {
     "eslint": "^9.0.0",
+    "@vitest/coverage-v8": "^3.0.0",
     "prettier": "^3.0.0",
     "vitest": "^3.0.0"
   }

Pin @vitest/coverage-v8 to the same major version as vitest to avoid internal mismatches.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package.json` at line 24, Add the missing `@vitest/coverage-v8` devDependency
(pin it to the same major version as the installed vitest) so the test:coverage
script can use provider: 'v8' from vitest.config.js; remove the ad-hoc --exclude
flags from the "test:coverage" script in package.json and instead add the
excluded test paths under coverage.exclude in vitest.config.js so excluded tests
don't run but are properly omitted from coverage instrumentation.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coderabbitai This was addressed in the same batch of commits:

  • @vitest/coverage-v8 added as devDependency (package.json line 38, pinned to ^3.2.4 matching vitest ^3.0.0 major)
  • coverage.include: ['src/**/*.js'] added to vitest.config.js

Re: --exclude flags → coverage.exclude: This is a false positive. The --exclude flags on vitest run prevent those test files from executing (they're integration/slow tests). coverage.exclude would only prevent instrumentation of source files but still run the tests. These serve different purposes — the current approach is correct.

Evidence:

# package.json line 38
"@vitest/coverage-v8": "^3.2.4",

# vitest.config.js lines 10-14
coverage: {
  provider: 'v8',
  reporter: ['text', 'lcov'],
  reportsDirectory: './coverage',
  include: ['src/**/*.js'],
},

# test:coverage script excludes integration tests from running, not from instrumentation
"test:coverage": "vitest run --coverage --exclude test/contracts.integration.test.js --exclude test/content.test.js --exclude test/version.test.js"

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the vitest.config.js content to confirm coverage.include is present
cat vitest.config.js

Length of output: 343


@flyingrobots You're absolutely right, and I apologize for the false positive. I've confirmed:

@vitest/coverage-v8@^3.2.4 is present in devDependencies (line 38), correctly pinned to match vitest's major version
coverage.include: ['src/**/*.js'] is configured in vitest.config.js (line 14)
Your --exclude usage is correct: those flags prevent slow integration tests from executing during coverage runs, which is exactly what you want. Using coverage.exclude would still run the tests but skip instrumenting source files—a completely different purpose.

My review was working from incomplete context. Thank you for the detailed correction with evidence. The configuration is solid.

    /\_/\
   ( o.o )  ← pedantic reviewer
    > ^ <     eating humble pie
   /|   |\
  (_|   |_)

Escalate no-unused-vars to error with varsIgnorePattern for _-prefixed
vars. Add @eslint/js and globals as explicit devDependencies per ESLint 9
best practices. Remove 8 unused imports and prefix 2 unused variables.
Add @vitest/coverage-v8 as devDependency, configure coverage.include
for src/**/*.js, add coverage step with artifact upload to CI workflow,
and enable npm cache in setup-node for faster installs.
…290)

Add git rev-parse --verify guard to reject malformed refs (including
option-injection via leading dashes) before passing SHA to git log.
Mirrors the validation pattern already used in src/epoch.js.
@flyingrobots
Copy link
Owner Author

@coderabbitai Thanks for the thorough review! Addressing all 8 comments across commits bf9f4e6..0c6a1a5.

Resolved (6 fixes):

  • eslint.config.js: no-unused-vars escalated to error + varsIgnorePattern: '^_' added; all 10 violations fixed (8 unused imports removed, 2 vars prefixed)
  • eslint.config.js: @eslint/js and globals added as explicit devDependencies
  • package.json: @vitest/coverage-v8 added as devDependency
  • vitest.config.js: coverage.include: ['src/**/*.js'] added
  • .github/workflows/ci.yml: npm cache enabled, coverage step + artifact upload added
  • src/cli/commands.js: rev-parse --verify guard added before git log call

Disputed (1 false positive) — remaining execSync in context.js and merge.js:

This was analyzed during the audit and intentionally scoped out. Here's the evidence:

File Line Command User Input? Protection
src/merge.js:19 git remote get-url origin No Hardcoded string, repoPath only used as cwd option
src/context.js:89 git ls-files No Hardcoded string, no interpolation
src/context.js:114 git log ... ${range} Yes sanitizeGitArg() rejects shell metacharacters; uses `2>/dev/null
src/context.js:136 git diff-tree ... ${sha} No SHA validated with /^[0-9a-f]+$/ regex at line 130

The context.js:114 call requires shell features (2>/dev/null || fallback) that execFileSync cannot provide without shell: true (which defeats the purpose). The sanitizer blocks ; & | ` $ ( ) { } ! # < > \n \r. Converting this to execFileSync would require restructuring the fallback logic, which is out of scope for this audit PR.

CI nit (npm cache): Also applied — cache: 'npm' added to actions/setup-node@v4.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 24, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Read the files mentioned in the user's response to verify their claims
echo "=== src/merge.js (line 19 area) ==="
cat src/merge.js | head -30

echo -e "\n=== src/context.js (lines 85-95 for line 89) ==="
sed -n '85,95p' src/context.js

echo -e "\n=== src/context.js (lines 110-120 for line 114 + sanitizeGitArg) ==="
sed -n '110,120p' src/context.js

echo -e "\n=== src/context.js (lines 130-140 for line 136) ==="
sed -n '130,140p' src/context.js

echo -e "\n=== Find sanitizeGitArg implementation ==="
rg -n -A 10 "function sanitizeGitArg|const sanitizeGitArg" src/

echo -e "\n=== Verify current state of commands.js (should have execFileSync) ==="
rg -n -C 3 "execFileSync|execSync" src/cli/commands.js

echo -e "\n=== Check for any shell metacharacters that might slip through ==="
rg -n "sanitizeGitArg" src/ -C 2

Length of output: 4636


🏁 Script executed:

#!/bin/bash
# Check all uses of sanitizeGitArg and test potential bypass vectors

echo "=== All uses of sanitizeGitArg ==="
rg -n "sanitizeGitArg\(" src/

echo -e "\n=== Check if backslash, space, or tab could enable git arg injection ==="
# Git ranges can include multiple revisions separated by spaces in some contexts
# Let's see what opts.range could be

echo "Searching for where extractCommitContext is called with range option:"
rg -n "extractCommitContext.*range" 

echo -e "\n=== Check if range option comes from user input ==="
rg -n "range:" --type js -A 2 -B 2

echo -e "\n=== Look for calls to extractCommitContext ==="
rg -n "extractCommitContext\(" --type js -C 3

Length of output: 5621


@flyingrobots Outstanding work on the 6 fixes—no-unused-vars escalation, devDependencies, coverage config, CI enhancements, and the rev-parse --verify guard are all solid improvements. 🎯


⚠️ DISPUTED: "Out of scope" unacceptable for security gaps in a security audit PR

You're asking me to accept incomplete command-injection protection in PR #291: "Audit: Guardrails & observability fixes"—a PR explicitly scoped as a security audit per issue #290. Let's dissect the remaining execSync calls:

File Line Verdict
src/merge.js:19 SAFE — hardcoded command, no interpolation
src/context.js:89 SAFE — hardcoded command, no interpolation
src/context.js:136 SAFE — SHA validated with /^[0-9a-f]+$/ (hex-only, sufficient)
src/context.js:114 🚨 INCOMPLETE PROTECTION — see below

🔴 Critical gap: sanitizeGitArg at line 114

Current implementation (line 105-109):

function sanitizeGitArg(value) {
  if (/[;&|`$(){}!#<>\n\r]/.test(value)) {
    throw new Error(`Unsafe characters in git argument: ${value}`);
  }
  return value;
}

Missing from blocklist:

  1. Space ( ) → enables argument injection:

    range = "HEAD --pretty=format:%H"
    // Becomes: git log --format="%h %s" HEAD --pretty=format:%H 2>/dev/null
    // Attacker controls --pretty, altering output format
  2. Tab (\t) → same as space, separates arguments

  3. Backslash (\) → potential escape sequences depending on shell context

Your defense: "Requires shell features (2>/dev/null || fallback), out of scope."

Counterargument:
This is a security audit PR. The shell fallback is refactorable:

let logOutput;
try {
  logOutput = execFileSync('git', ['log', '--format=%h %s', range], { cwd, encoding: 'utf-8' });
} catch {
  logOutput = execFileSync('git', ['log', '--format=%h %s', `-${limit}`], { cwd, encoding: 'utf-8' });
}

No shell needed. No || operator. No incomplete sanitizer. Problem solved.

Alternatively, if refactoring is genuinely deferred, strengthen the sanitizer:

- if (/[;&|`$(){}!#<>\n\r]/.test(value)) {
+ if (/[;&|`$(){}!#<>\s\\]/.test(value)) {  // Added \s (space/tab) and \\

🎯 Required action:

Choose one of the following before I approve this PR:

  1. Option A (ideal): Refactor context.js:114 to use execFileSync with try-catch (eliminates shell entirely)
  2. Option B (acceptable): Strengthen sanitizeGitArg to block \s and \ (add test cases for space/tab injection)
  3. Option C (minimum): File a high-priority follow-up issue tracking the incomplete sanitization, linked to this PR, and assigned to you—then I'll mark this resolved

"Out of scope" doesn't fly when the PR title is literally "audit guardrails." You don't leave half-fixed command injection in a security audit. 🔥


Awaiting your call: A, B, or C?

…ion vectors (#290)

- context.js: all 3 execSync calls converted to execFileSync with args arrays
- context.js: shell fallback (2>/dev/null || ...) replaced with try-catch
- context.js: sanitizeGitArg strengthened to block \s and \ (defense-in-depth)
- merge.js: execSync for git-remote-get-url converted to execFileSync
- test: added assertions for space, tab, and backslash injection vectors
@flyingrobots
Copy link
Owner Author

@coderabbitai Option A implemented — full execFileSync refactor in commit 6cfc2be.

Changes:

File Before After
src/context.js:89 execSync('git ls-files', ...) execFileSync('git', ['ls-files'], ...)
src/context.js:118-121 execSync('git log ... ${range} 2>/dev/null || git log ...', ...) try-catch with two execFileSync calls
src/context.js:135-138 execSync('git diff-tree ... ${sha}', ...) execFileSync('git', ['diff-tree', ...args, sha], ...)
src/merge.js:19 execSync('git remote get-url origin', ...) execFileSync('git', ['remote', 'get-url', 'origin'], ...)
sanitizeGitArg Missing \s, \ Added \s and \\ to blocklist (defense-in-depth)

Test evidence:

  • New test: rejects range values with spaces, tabs, and backslashes
  • All existing tests pass: 578/578 (29 files)
  • Lint clean

Zero execSync calls remain in production source. The sanitizeGitArg is now purely defense-in-depth since execFileSync never invokes a shell.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 24, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash

echo "=== 1. Verify NO execSync remains in production source (src/) ==="
rg -n "execSync" src/ || echo "✅ No execSync found in src/"

echo -e "\n=== 2. Check all execFileSync usage in production code ==="
rg -n "execFileSync" src/

echo -e "\n=== 3. Verify sanitizeGitArg implementation (should include \\s and \\\\) ==="
rg -n -A 5 "function sanitizeGitArg|const sanitizeGitArg" src/

echo -e "\n=== 4. Verify src/context.js refactored sections ==="
echo "--- Line 89 area (git ls-files) ---"
sed -n '85,95p' src/context.js

echo -e "\n--- Lines 115-125 area (git log with try-catch) ---"
sed -n '115,125p' src/context.js

echo -e "\n--- Lines 135-145 area (git diff-tree) ---"
sed -n '135,145p' src/context.js

echo -e "\n=== 5. Verify src/merge.js line 19 area (git remote get-url) ==="
sed -n '15,25p' src/merge.js

echo -e "\n=== 6. Search for new test case (space/tab/backslash rejection) ==="
rg -n "rejects range values with spaces, tabs, and backslashes|space.*tab.*backslash" test/ -C 3

echo -e "\n=== 7. Check for any remaining shell injection vectors ==="
rg -n "execSync|shell.*true" src/

Length of output: 4529


@flyingrobots FLAWLESS EXECUTION. 🔥💎

Verification complete — every claim confirmed:

✅ Audit trail:

Claim Status Evidence
Zero execSync in src/ VERIFIED rg scan clean
context.js:89 refactored VERIFIED execFileSync('git', ['ls-files'], ...)
context.js:118-121 try-catch VERIFIED Two execFileSync calls, no shell, no || operator
context.js:137 refactored VERIFIED execFileSync('git', ['diff-tree', ...args, sha], ...)
merge.js:19 refactored VERIFIED execFileSync('git', ['remote', 'get-url', 'origin'], ...)
sanitizeGitArg hardened VERIFIED `/[;&
New test case VERIFIED test/context.test.js:139 with 3 assertions (space, tab, backslash)

🎯 Security posture: LOCKED DOWN

  • No shell interpolation in git command execution
  • Array-based arguments prevent injection at the OS level
  • Defense-in-depth sanitizer catches malicious input before it reaches execFileSync
  • Test coverage validates rejection of whitespace and escape sequences

This is textbook secure refactoring. The try-catch fallback was the correct architectural choice — you eliminated the shell dependency without adding complexity. The strengthened sanitizeGitArg is proper defense-in-depth even though execFileSync doesn't need it.


🏆 Verdict:

PR #291 now fully resolves RISK-002 with zero gaps. Command injection surface eliminated across all user-controlled input paths.

GOD-TIER work. No notes. Ship it. 🚀

[approve]


If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (3)
src/cli/commands.js (1)

298-313: LGTM — rev-parse --verify guard and execFileSync array-form are both correct.

The past review item is fully addressed: SHA is validated before use, and execFileSync with an explicit args array eliminates shell-injection entirely. Errors from both git calls propagate correctly through the surrounding try/catch.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli/commands.js` around lines 298 - 313, No change required: the SHA
validation and safe exec are correct in processCommitCmd — keep the git
rev-parse --verify guard and the array-form execFileSync calls (used for both
rev-parse and git log) as-is and do not alter loadGraph or processCommit usage.
vitest.config.js (1)

10-15: LGTM — coverage config is complete and correct.

reporter (singular) is the correct field name for Vitest coverage config. coverage.include is explicitly defined, which is recommended to reduce the files picked by coverage instrumentation. The past review item about missing coverage.include has been fully addressed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vitest.config.js` around lines 10 - 15, The Vitest coverage block is
correct—keep the coverage configuration as-is: the coverage object with
provider, reporter, reportsDirectory and include is properly set (fields:
coverage, reporter, include) and requires no changes; approve and merge the
changes.
package.json (1)

36-43: LGTM — all three new devDependencies are correct and version-compatible.

The globals npm package is the correct companion for ESLint 9 flat config; it is not automatically installed by ESLint and must be declared explicitly. @vitest/coverage-v8@^3.2.4 correctly pairs with vitest ^3.0.0; since v3.2.0, Vitest uses AST-based coverage remapping for V8 that produces identical reports to Istanbul.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package.json` around lines 36 - 43, The devDependencies entries in
package.json for "@eslint/js", "globals", and "@vitest/coverage-v8" are correct
and compatible with "eslint" and "vitest"; no code changes required—leave the
"devDependencies" block as-is, approve/merge the PR, and optionally add a brief
PR note mentioning that "globals" is intentionally declared for ESLint flat
config and that "@vitest/coverage-v8@^3.2.4" pairs with "vitest@^3.0.0".
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/ci.yml:
- Around line 46-50: Add an explicit "if: always()" condition to the "Upload
coverage report" step (the step with name "Upload coverage report" that uses
actions/upload-artifact@v4) so the artifact upload runs even when earlier steps
fail; modify that step's metadata to include if: always() so partial
coverage/lcov.info is uploaded on test failures for diagnosis.
- Around line 40-44: The CI workflow currently runs the full test suite twice:
the "Run tests" step invoking `npm test` and the "Run tests with coverage" step
invoking `npm run test:coverage`; remove the redundant `npm test` step (or
replace it with the coverage step) so the matrix runs only `npm run
test:coverage` for both pass/fail gating and coverage reporting, updating the
step named "Run tests" or deleting it and retaining the "Run tests with
coverage" step accordingly.

In `@src/context.js`:
- Around line 104-109: sanitizeGitArg currently blocks shell metacharacters but
allows leading hyphens, enabling option-injection when its output (the `range`
value) is passed to `git log` via `execFileSync`; update sanitizeGitArg to also
reject values that begin with a hyphen (e.g., /^-/) and throw the same Error for
consistency, then ensure the `git log` call that uses the `range` array still
falls back to the numeric `-${limit}` path for safe cases; add regression tests
for sanitizeGitArg (and the code path that invokes `git log`) asserting that
values like `--all`, `--all-match`, `-n5` (and other leading-dash git options)
are rejected.

In `@src/suggest.js`:
- Line 9: The comment "// edges.js imported for future use in context-aware
suggestions" is misleading because there is no actual import of edges.js; remove
or replace it with a forward-declaration that doesn't imply an import (e.g., "//
edges.js will be used in future for context-aware suggestions" or remove the
line entirely) in src/suggest.js, ensuring no mention of "imported" when no
import exists so readers won't search for a non-existent import.

---

Duplicate comments:
In `@package.json`:
- Around line 36-43: The devDependencies entries in package.json for
"@eslint/js", "globals", and "@vitest/coverage-v8" are correct and compatible
with "eslint" and "vitest"; no code changes required—leave the "devDependencies"
block as-is, approve/merge the PR, and optionally add a brief PR note mentioning
that "globals" is intentionally declared for ESLint flat config and that
"@vitest/coverage-v8@^3.2.4" pairs with "vitest@^3.0.0".

In `@src/cli/commands.js`:
- Around line 298-313: No change required: the SHA validation and safe exec are
correct in processCommitCmd — keep the git rev-parse --verify guard and the
array-form execFileSync calls (used for both rev-parse and git log) as-is and do
not alter loadGraph or processCommit usage.

In `@vitest.config.js`:
- Around line 10-15: The Vitest coverage block is correct—keep the coverage
configuration as-is: the coverage object with provider, reporter,
reportsDirectory and include is properly set (fields: coverage, reporter,
include) and requires no changes; approve and merge the changes.

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c00609d and 6cfc2be.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (12)
  • .github/workflows/ci.yml
  • eslint.config.js
  • package.json
  • src/cli/commands.js
  • src/context.js
  • src/dag.js
  • src/doctor.js
  • src/merge.js
  • src/nodes.js
  • src/suggest.js
  • test/context.test.js
  • vitest.config.js

- ci.yml: collapse redundant npm test + test:coverage into single coverage step
- ci.yml: add if: always() to upload step for diagnosis on failure
- context.js: block leading hyphens in sanitizeGitArg to prevent option injection
- suggest.js: fix misleading "imported" comment to TODO forward-declaration
- test: add regression tests for option-injection vectors (--all, -n99999, etc.)
@flyingrobots
Copy link
Owner Author

@coderabbitai review please 🥕

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 24, 2026

@flyingrobots Absolutely! I'll review the current state of the PR for you. 🥕

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Audit: Guardrails & observability fixes

1 participant