class Analyzer::Javascript::Cli

Overview

Surfaces the command-line attack surface of JavaScript/TypeScript programs (Node, Deno, Bun) as cli:// endpoints: one endpoint per (sub)command with named options (param_type "flag"), positional arguments ("argument") and consumed environment variables ("env"). Covers the util.parseArgs builtin plus commander, yargs, cac, sade, meow, minimist, arg, command-line-args, getopts and citty. A single analyzer scans every JS/TS extension so a .ts CLI isn't double-counted.

Defined in:

analyzer/analyzers/javascript/cli.cr

Constant Summary

ARG_CALL_RE = /\barg\s*\(\s*\{/

arg: arg({ '--name': String, '-n': '--name' }). Keys whose value is a quoted string are aliases pointing at the canonical flag and are skipped so an alias doesn't surface as a second, meaningless flag.

ARG_KEY_RE = /^\s*['"](-{1,2}[A-Za-z][\w-]*)['"]\s*:\s*(.+?),?\s*$/
ARGUMENT_RE = /\.\s*argument\s*\(\s*['"][<\[]?\.{0,3}([A-Za-z0-9_-]+)/
ARGV_SLICE = /\bprocess\.argv\.slice\s*\(\s*2\s*\)/
BUN_ARGV = /\bBun\.argv\b/
BUN_ENV = /\bBun\.env\.([A-Za-z_][A-Za-z0-9_]*)/
CITTY_ARGS_HEADER = /\bargs\s*:\s*\{/
CITTY_DEFINE_ASSIGN_RE = /^\s*(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*defineCommand\s*\(\s*\{/

citty also allows each subcommand to be declared as its own top-level const NAME = defineCommand({...}) and merely referenced by identifier inside subCommands: { key: NAME }, instead of nesting the defineCommand call inline. resolve_citty_var_subcommands resolves those same-file references so args: inside NAME's block attributes to <root>/<key> rather than falling back to root.

CITTY_SUBCOMMAND_RE = /^\s*['"]?([A-Za-z_$][\w$-]*)['"]?\s*:\s*defineCommand\s*\(\s*\{/

citty: serve: defineCommand({ args: { port: { type: 'string' } } }). Subcommands nest their own defineCommand object inside subCommands, so args are attributed via a brace-depth stack (never a sticky cursor).

CITTY_SUBCOMMAND_REF_RE = /^\s*['"]?([A-Za-z_$][\w$-]*)['"]?\s*:\s*([A-Za-z_$][\w$]*)\s*,?\s*$/
CITTY_SUBCOMMANDS_HEADER = /\bsubCommands\s*:\s*\{/
CLA_ARRAY_ASSIGN_RE = /^\s*(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*\[/

command-line-args: commandLineArgs(optionDefinitions) referencing a separately-declared const optionDefinitions = [...] array (the library's own documented convention), or an inline commandLineArgs([...]) array. Matching is bounded to the resolved array's own brackets (never a whole-file scan) so an unrelated same-shaped object literal elsewhere in the file (e.g. a content-field schema) is never picked up as a bogus flag, and each {...} entry is scanned as its own brace-bounded block so Prettier-style multi-line-per-key formatting isn't silently dropped.

CLA_CALL_INLINE_RE = /\bcommandLineArgs\s*\(\s*\[/
CLA_CALL_VAR_RE = /\bcommandLineArgs\s*\(\s*([A-Za-z_$][\w$]*)\s*[,)]/
CLA_NAME_RE = /\bname\s*:\s*['"]([A-Za-z0-9_-]+)['"]/
CLA_TYPE_KEY_RE = /\btype\s*:/
CLI_MARKERS_RE = Regex.union("commander", "yargs", "cac", "meow", "minimist", "clipanion", "@oclif", "sade", "parseArgs", "Deno.args", "Bun.argv")

Precompiled once — a single PCRE2-JIT scan replaces up to eleven naive String#includes? substring scans over the whole file content. Regex.union auto-escapes each literal (including the dots in "Deno.args"/"Bun.argv"), so it is provably equivalent to the OR-of-includes? it replaces.

COMMAND_RE = /\.\s*command\s*\(\s*['"]([^'"]+)['"]/

commander / cac / sade / yargs subcommand. The command string may carry an arg spec (serve <port>), so the first whitespace token is the name.

DENO_ARGS = /\bDeno\.args\b/

builtin / runtime argv markers (root surface).

DENO_ENV = /\bDeno\.env\.get\s*\(\s*['"]([^'"]+)['"]/
ENV_DOT = /\bprocess\.env\.([A-Za-z_][A-Za-z0-9_]*)/

env reads (gated). NODE_ENV is config plumbing, not an input.

ENV_INDEX = /\bprocess\.env\s*\[\s*['"]([A-Za-z_][A-Za-z0-9_]*)['"]\s*\]/
GETOPTS_ALIAS_HEADER = /\balias\s*:\s*\{/
GETOPTS_BOOLEAN_HEADER = /\bboolean\s*:\s*\[/
GETOPTS_CALL_RE = /\bgetopts\s*\(/

getopts: getopts(process.argv.slice(2), { alias: {...}, default: {...}, boolean: [...], string: [...] }). Bounded to the call's own parens so unrelated alias/default object literals elsewhere aren't picked up.

GETOPTS_DEFAULT_HEADER = /\bdefault\s*:\s*\{/
GETOPTS_STRING_HEADER = /\bstring\s*:\s*\[/
LIB_IMPORT_ONLY_RE = /(?:require\s*\(\s*|from\s+)['"](?:arg|getopts|citty|command-line-args)['"]/

require('arg') / from 'arg', and likewise for getopts/citty/ command-line-args — these are short/generic tokens too easy to trip as a bare substring (e.g. bash's own getopts builtin, a stray comment, or an ordinary identifier fragment), so they require an actual import statement rather than content.includes?.

LONG_FLAG_RE = /(--[A-Za-z0-9][\w-]*)/
MEOW_FLAGS_HEADER = /\bflags\s*:\s*\{/
NEW_CHAIN_RE = /^[A-Za-z_$][\w$]*\s*\.\s*(?:command|option|argument)\b/

A fresh statement that chains command/option/argument off an identifier (program.option(...), cli.command(...)) starts at the root program, not the previously-seen subcommand — used to reset the cursor so a global option declared after a subcommand isn't mis-attributed to it.

OBJECT_KEY_RE = /^\s*['"]?([A-Za-z_$][\w$-]*)['"]?\s*:/
OBJECT_VALUE_RE = /^\s*['"]?[A-Za-z_$][\w$-]*['"]?\s*:\s*['"]([A-Za-z0-9_-]+)['"]/
OPTION_RE = /\.\s*(?:required)?[Oo]ption\s*\(\s*['"]([^'"]+)['"]/

commander/cac/sade: .option('-p, --port <n>'); yargs: .option('port').

PARSEARGS_HEADER = /\boptions\s*:\s*\{/

object-literal schema headers (keys become root flags).

POSITIONAL_RE = /\.\s*positional\s*\(\s*['"]([^'"]+)['"]/
SHORT_FLAG_RE = /(-[A-Za-z0-9])\b/
SOURCE_EXTS = [".js", ".mjs", ".cjs", ".jsx", ".ts", ".mts", ".cts", ".tsx"]
STRING_LIT_RE = /['"]([A-Za-z0-9_.\/-]+)['"]/
WEB_FRAMEWORK_RE = /(?:require\s*\(\s*|from\s+)['"](?:express|fastify|koa|@hapi\/hapi|@nestjs\/[\w-]+|next|nuxt|hono|@hono\/[\w-]+|restify|@adonisjs\/[\w-]+|elysia|polka|connect|h3|@sveltejs\/[\w-]+|@remix-run\/[\w-]+|apollo-server|@apollo\/server)['"]/
WEB_LISTEN_RE = /\.\s*listen\s*\(|\bcreateServer\s*\(/

Instance Method Summary

Instance methods inherited from class Analyzer

analyze analyze, base_path : String base_path, base_paths : Array(String) base_paths, callees_needed? : Bool callees_needed?, http_header_name(name : String) : String | Nil http_header_name, line_number_for_index(content : String, char_index : Int32) : Int32 line_number_for_index, logger : NoirLogger logger, parallel_analyze(files : Array(String), &block : String -> Nil) parallel_analyze, read_file_content(path : String) : String read_file_content, result : Array(Endpoint) result, unique_params(params : Array(Param)) : Array(Param) unique_params, url : String url, web_root_path(path : String, markers : Array(String)) : String web_root_path

Constructor methods inherited from class Analyzer

new(options : Hash(String, YAML::Any)) new

Instance methods inherited from module FileHelper

all_files : Array(String) all_files, get_files_by_extension(extension : String) : Array(String) get_files_by_extension, get_files_by_extensions(extensions : Array(String)) : Array(String) get_files_by_extensions, get_files_by_prefix(prefix : String) : Array(String) get_files_by_prefix, get_files_by_prefix_and_extension(prefix : String, extension : String) : Array(String) get_files_by_prefix_and_extension, get_public_dir_files(base_path : String, folder : String) : Array(String) get_public_dir_files, get_public_files(base_path : String, anchors : Array(String) = ["shard.yml", "Gemfile"]) : Array(String) get_public_files

Instance Method Detail

def analyze #

[View source]