class Analyzer::Java::Cli

Overview

Surfaces the command-line attack surface of Java programs as cli:// endpoints: one endpoint per (sub)command with named options (param_type "flag"), positional arguments ("argument") and consumed environment variables ("env"). Covers picocli, args4j, JCommander, commons-cli, airline and jopt-simple, plus gated System.getenv reads.

Line-scan analyzer (Go/Ruby/Rust CLI house style) merging endpoints by URL. Subclasses Analyzer directly (JavaEngine is a module) and uses JavaEngine.test_path? to skip tests.

Defined in:

analyzer/analyzers/java/cli.cr

Constant Summary

ADD_OPTION_LL = /\.addOption\s*\(\s*"([^"]+)"\s*,\s*"([^"]+)"\s*,/

commons-cli (no subcommands; flags on root).

ADD_OPTION_SHORT = /\.addOption\s*\(\s*"([^"]+)"\s*,\s*(?:true|false)\s*,/

Current (non-deprecated) addOption(String opt, boolean hasArg, String description) overload used for short-only flags. The true|false literal gate in the 2nd argument position guarantees this never overlaps with ADD_OPTION_LL above (which requires a quoted string there), so a given call is matched by exactly one of the two.

ARGUMENT_ATTR = /@Argument\b/
ARGUMENTS_ATTR = /@Arguments\s*\(([^)]*)\)/
CLI_GATE_RE = /@Command\b|@Parameter\b|new\s+JCommander|new\s+CmdLineParser|new\s+Options\s*\(/
COMMAND_ATTR = /@Command\s*\([^)]*\bname\s*=\s*"([^"]+)"/
COMMAND_NAME_KV = /\bname\s*=\s*"([^"]+)"/
COMMAND_OPEN = /@Command\b/
FIELD_DECL = /^\s*(?:public|private|protected)?\s*(?:final\s+|static\s+)*[\w<>\[\].]+\s+(\w+)\s*[;=]/
GET_ENV = /\bSystem\.getenv\s*\(\s*"([^"]+)"\s*\)/
JC_COMMAND = /@Parameters\s*\([^)]*\bcommandNames\s*=\s*\{?\s*"([^"]+)"/
JOPT_ACCEPTS = /\b(\w+)\.accepts\s*\(\s*"([^"]+)"/
JOPT_ACCEPTS_ALL = /\b(\w+)\.acceptsAll\s*\(\s*(?:Arrays\.asList|List\.of|Collections\.singletonList)\s*\(\s*"([^"]+)"/
JOPT_PARSER_DECL = /\b(\w+)\s*=\s*new\s+OptionParser\s*\(/

jopt-simple (no subcommands; flags land on root, like commons-cli). .accepts("flag")/.acceptsAll(List.of("f","flag")) are only real CLI flags when called on a variable that was actually bound to new OptionParser(...) — jopt-simple's OptionParser isn't the only class with an accepts(...) method (e.g. a FormatMatcher.accepts(fmt) helper), so the receiver is tracked per-file in the same forward pass and matches on an untracked receiver are dropped rather than attributed to the root command.

LIB_MARKERS = ["picocli.", "org.kohsuke.args4j", "com.beust.jcommander", "org.apache.commons.cli", "com.github.rvesse.airline", "io.airlift.airline", "joptsimple."]
LIB_MARKERS_RE = Regex.union(LIB_MARKERS)
LONG_OPT = /\.longOpt\s*\(\s*"([^"]+)"/
OPTION_ATTR = /@Option\s*\(([^)]*)\)/
PARAMETER_ATTR = /@Parameter\s*\(([^)]*)\)/
PARAMETERS_ATTR = /@Parameters\b([^)\n]*\)?)/
RAW_CLI_MARKERS_RE = Regex.union(LIB_MARKERS + ["@Command", "@Parameter", "JCommander", "CmdLineParser", "Options"])

Cheap pre-gate applied to the RAW file, before the comment strip.

strip_comments materialises an Array(Char) of the whole file and rebuilds it character by character; running it on all 8,658 .java files in spring-boot to then reject 97% of them on the gate below was the bulk of this analyzer. Stripping only replaces characters with ' ' or '\n' and never changes the character count, so a literal with no whitespace in it can appear in the stripped text only if it already appears, unchanged, in the raw text. Each alternative of the real gate requires one of these literals verbatim (the new\s+X forms require X; the \s-carrying parts are deliberately not relied on, since blanking a comment can manufacture whitespace). Files that pass still go through the full, comment-aware gate.

SUBCOMMANDS_KEY = /\bsubcommands\s*=/
WEB_FRAMEWORK_RE = /\bimport\s+(?:org\.springframework|jakarta\.ws\.rs|javax\.ws\.rs|io\.quarkus|io\.micronaut|io\.javalin|io\.vertx|com\.linecorp\.armeria|io\.dropwizard|spark\.|org\.apache\.struts)/

Class Method Summary

Instance Method Summary

Instance methods inherited from class Analyzer

analyze analyze, base_path : String base_path, base_paths : Array(String) base_paths, base_relative_path(path : String) : String base_relative_path, callees_needed? : Bool callees_needed?, content_matches?(content : String, markers : Regex) : Bool content_matches?, 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

Macros inherited from class Analyzer

analyzer_for(tech) analyzer_for

Instance methods inherited from module FileHelper

all_files : Array(String) all_files, get_files_by_basename(basename : String) : Array(String) get_files_by_basename, 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_files_by_relative_path(relative_path : String, root : String = "") : Array(String) get_files_by_relative_path, 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, walked_path(expanded : String) : String walked_path

Class Method Detail

def self.tech_name : String #

[View source]

Instance Method Detail

def analyze #

[View source]