Skip to content

sh-ast@0.2.0

Choose a tag to compare

@github-actions github-actions released this 22 Jul 15:54
· 4 commits to main since this release
58bb767

Minor Changes

  • #25 ea7dbfb Thanks @mike-north! - sh-ast/analyze now re-exports ShAstError (the shared base class every error the analyze layer throws — ShAnalyzeMaxDepthError, ShAnalyzeInvalidWrapperSpecError — extends) and ShNode/Position (the node/position types the subpath's public surface consumes and exposes). A consumer of sh-ast/analyze can now catch/reference these directly, without also importing from the root sh-ast entry point. This also clears the two pre-existing, accepted ae-forgotten-export API Extractor warnings at the top of packages/sh-ast/api-report/sh-ast-analyze.api.md (see #23).

  • #16 ed46136 Thanks @mike-north! - Add resolveArgv0 to sh-ast/analyze: follows a CommandSite's argv0 through zero or more transparent wrappers (env, sudo, nohup, nice, command, exec, time, timeout) to the effective command actually invoked — the one a permission/policy check must judge, since argv0 alone is trivially spoofable through any of these (env FOO=1 rm -rf /, sudo -u x "$prog", …).

    The default wrapper table (DEFAULT_TRANSPARENT_WRAPPERS, exported alongside the new WrapperSpec type) is plain data — each entry's flag/operand handling is hand-derived from that wrapper's own manual page — and is fully overridable/extensible via resolveArgv0's transparentWrappers option; xargs is deliberately excluded (its argument-splicing semantics make "the wrapped command" a stdin-dependent, not statically locatable, concept). A statically-unknowable word anywhere in the chain (an expansion, a glob, …) is never guessed through: it becomes Argv0Resolution.effective immediately, and Argv0Resolution.chain stops there. Argv0Resolution.assignmentsSkipped counts CallExpr.assigns shell-assignment prefixes (FOO=bar rm x), a mechanism distinct from a wrapper's own VAR=val operands (env A=1 rm x).

    Facts only, matching resolveWord's and enumerateCommands's posture: no safety verdict, no hardcoded "dangerous command" list.

    Hardened against several ways an unrecognized or unusual invocation could previously misreport the effective command:

    • A statically known word shaped like a flag (--prefixed, not --) that doesn't match any flag/operand shape a WrapperSpec recognizes now makes the whole resolution unresolvable (Argv0Resolution.effective becomes { static: false, reason: 'unknown-flag' }), instead of being silently treated as the wrapped command — e.g. sudo -D /tmp rm x (-D isn't a modeled sudo flag) no longer reports rm as effective. Argv0ChainWord and the new Argv0UnresolvedReason/Argv0UnresolvedWord types widen Argv0Resolution.chain/.effective to carry this.
    • argFlags now recognizes every standard getopt short-option form, not just the exact and separate-word forms: attached (-uuser), clustered with preceding no-operand flags (-Eu user/-Euuser), matching real option parsing.
    • env's -S/--split-string is no longer modeled as an ordinary operand-taking flag: its value splices into the invoked command's own argv (GNU env(1)), so the real command is embedded inside the operand text, not a separate word — it now reports reason: 'embedded-command' instead of guessing.
    • command -v/-V (which print information about a command name rather than execute it) now correctly stop the chain at command itself via the new WrapperSpec.stopsChainFlags field, instead of continuing to whatever word follows.
    • WrapperSpec.names matching is now documented as exact-name-only (sudo never matches /usr/bin/sudo), and DEFAULT_TRANSPARENT_WRAPPERS is now deep-frozen.
    • A caller-supplied transparentWrappers table with a malformed entry now throws the new ShAnalyzeInvalidWrapperSpecError at the resolveArgv0 boundary, instead of failing confusingly deep inside flag matching.
  • #21 163f842 Thanks @mike-north! - Renamed the public error taxonomy exported from sh-ast to drop pre-spinout eslint-sh/@eslint-sh/bridge naming, now that this package ships standalone. This is a breaking rename with no back-compat aliases — 0.x, so it ships as minor rather than major per this repo's pre-1.0 policy.

    code string literals:

    • ESLINT_SH_PARSE_ERRORSH_AST_PARSE_ERROR
    • ESLINT_SH_INVALID_DIALECTSH_AST_INVALID_DIALECT
    • ESLINT_SH_BRIDGE_INTERNALSH_AST_INTERNAL
    • ESLINT_SH_ANALYZE_MAX_DEPTHSH_AST_ANALYZE_MAX_DEPTH
    • ESLINT_SH_PARSE_MAX_DEPTHSH_AST_PARSE_MAX_DEPTH
    • ESLINT_SH_ANALYZE_INVALID_WRAPPER_SPECSH_AST_ANALYZE_INVALID_WRAPPER_SPEC

    Class renames:

    • ShBridgeError (abstract base) → ShAstError
    • ShBridgeInternalErrorShInternalError

    All other exported error classes (ShParseError, ShInvalidDialectError, ShAnalyzeMaxDepthError, ShParseMaxDepthError, ShAnalyzeInvalidWrapperSpecError) are unchanged — they were already product-neutral. No error semantics, messages, or throw sites changed; this is an identifier rename only.

  • #18 ef343d7 Thanks @mike-north! - parseSync now rejects pathologically deep/nested shell source (deeply nested subshells, command substitutions, control-flow bodies, or very long pipeline/list chains) with a typed, catchable ShParseMaxDepthError (code: 'SH_AST_PARSE_MAX_DEPTH') instead of letting it reach the WASM parser, where sufficiently deep nesting causes an uncatchable native stack overflow. The guard runs a conservative, single-pass estimate of the input's structural nesting depth before ever invoking the shared WASM instance, so pathological input never risks crashing (or wedging) that instance for subsequent calls. Realistic scripts, including deeply-but-legitimately nested ones, are unaffected.

    Hardened against a bypass found in review: an unmatched closer (} with no open {, or a stray fi/done/esac with no matching opener) previously decremented the depth estimate unconditionally, letting a self-canceling adversarial input (e.g. case x in a}) repeated) silently defeat the guard while the real parser still recursed to a genuine, uncatchable stack overflow — every closer now only decrements state when it actually matches an open region. Also fixed: | alternation inside a case arm's pattern list (a|b|c) ...) was incorrectly counted as pipeline depth, falsely rejecting arms with many alternatives — a real pipeline in the arm's action list still counts correctly.