class Analyzer::Groovy::Cli

Overview

Surfaces the command-line attack surface of Groovy programs as cli:// endpoints: the built-in CliBuilder (and picocli @Option), JCommander (@Parameter / addCommand subcommands) and Commons CLI (Option.builder / addOption), plus System.getenv. Line-scan; attribution for JCommander subcommands additionally uses a per-file pre-scan (variable -> class -> command name) so it can resolve both the inline addCommand("name", new Class()) form and the more common declare-then-register addCommand("name", instance) form. Root attribution otherwise (CliBuilder/Commons CLI are flat), merged by URL.

NOTE like CliBuilder, this is a per-file analyzer. When a JCommander subcommand class lives in its own file (registration call in one file, @Parameter fields in another), the class-to-command mapping built here won't span files, so that subcommand's fields fall back to that file's own root endpoint instead of being attributed under the real subcommand's URL. A cross-file pre-pass would be needed to close that gap; not attempted here.

Defined in:

analyzer/analyzers/groovy/cli.cr

Constant Summary

CLASS_DECL = /\bclass\s+([A-Za-z_]\w*)/
CLI_MARKER = /\bimport\s+org\.apache\.commons\.cli\b|\bOption\.builder\s*\(/

--- Commons CLI ------------------------------------------------------

CLI_OPT = /\bcli\.([A-Za-z_]\w*)\s*\(([^)]*)/
CLI_TEST_PATH_RE = Regex.union("/test/", "spec.groovy", "test.groovy")

One precompiled Regex.union scan (PCRE2 JIT) replaces three separate String#includes? scans of the same buffer -- Crystal's includes? is not Boyer-Moore accelerated, so a single regex pass over the downcased path is cheaper than three. Equivalent to the OR-of-substrings it replaces (union escapes each literal).

COMMONS_CLI_ADD_OPT = /\.addOption\s*\(\s*['"]([^'"]+)['"]\s*,\s*['"]([^'"]+)['"]\s*,/
COMMONS_CLI_BUILDER = /Option\.builder\s*\(\s*(?:['"]([^'"]*)['"])?\s*\)/
COMMONS_CLI_LONGOPT = /\.longOpt\s*\(\s*['"]([^'"]+)['"]/
GET_ENV = /\bSystem\.getenv\s*\(\s*['"]([^'"]+)['"]/
JC_ADD_CMD_STR = /\.addCommand\s*\(\s*['"]([^'"]+)['"]\s*,\s*new\s+([A-Za-z_]\w*)\s*\(/
JC_ADD_CMD_VAR = /\.addCommand\s*\(\s*['"]([^'"]+)['"]\s*,\s*([A-Za-z_]\w*)\s*\)/
JC_COMMAND_NAMES = /@Parameters\s*\([^)]*\bcommandNames\s*=\s*\{?\s*['"]([^'"]+)['"]/
JC_MARKER = /\bimport\s+com\.beust\.jcommander\b|\bnew\s+JCommander\s*\(|\bJCommander\.newBuilder\s*\(/

--- JCommander ----------------------------------------------------- Gated on library-specific constructs only (never a bare @Parameter, which is too generic on its own) so unrelated annotations/classes named similarly don't light this up.

JC_PARAMETER = /@Parameter\s*\(([^)]*)\)/
LONGOPT = /longOpt:\s*['"]([^'"]+)['"]/
MARKERS = /\bnew\s+CliBuilder\b|\bCliBuilder\s*\(|@picocli|@Command\b/
NON_OPTION = Set {"parse", "usage", "with", "width", "header", "footer", "stopAtNonOption", "expandArgumentFiles", "posix", "errorWriter", "writer", "name"}

CliBuilder methods that are not option definitions.

OPTION_ATTR = /@Option\s*\(([^)]*)\)/
VAR_NEW_DECL = /\b[A-Za-z_]\w*\s+([A-Za-z_]\w*)\s*=\s*new\s+([A-Za-z_]\w*)\s*\(/
WEB_RE = /\bimport\s+(?:grails|org\.springframework)\b|@Controller\b|@RestController\b/

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]