feat(cli): make string deobfuscation opt-in and drop the prompt

The interactive deobfuscation prompt (and the confusing -y/--yes /
--no-prompt flag that skipped it) is removed entirely. String deobfuscation
is now opt-in: a plain run defaults to static (and language) strings only,
and stack/tight/decoded must be requested explicitly via --string-type.
--analyze-functions requests function-level deobfuscation, so it is not
disabled by the default. This resolves the silent redirect behavior from
#1176: there is no implicit 'n' anymore, just an explicit default.
This commit is contained in:
lakshit verma 2026-08-19 15:13:21 +05:30
parent f700666757
commit 0abe0c2103
No known key found for this signature in database
4 changed files with 19 additions and 62 deletions

View file

@ -378,13 +378,6 @@ def make_parser():
logging_group.add_argument(
"-q", "--quiet", action="store_true", help="disable all status output on STDOUT except fatal errors"
)
logging_group.add_argument(
"--no-prompt",
dest="no_prompt",
action="store_true",
default=False,
help="do not prompt to enable string deobfuscation (defaults to not running it)",
)
logging_group.add_argument(
"--color",
type=str,

View file

@ -211,10 +211,13 @@ def main(argv=None) -> int:
disabled_string_types = expand_string_types(list(args.disabled_string_types or []))
enabled_string_types = expand_string_types(list(args.enabled_string_types or []))
if args.summary and not disabled_string_types and not enabled_string_types:
# the summary's layout-derived sections cover static strings only, so
# don't spin up the slow deobfuscation for stack/tight/decoded
logger.info("--summary is static-only; skipping stack/tight/decoded extraction")
if not enabled_string_types and not disabled_string_types and not args.analyze_functions:
# string deobfuscation is opt-in: default to static (and language)
# strings only, so a plain run doesn't spin up the slow vivisect stage
# for stack/tight/decoded. enable it explicitly with
# --string-type stack/tight/decoded (--analyze-functions requests
# function-level deobfuscation and is therefore not affected).
logger.info("string deobfuscation is off by default; use --string-type stack/tight/decoded to enable")
disabled_string_types.extend([StringType.STACK.value, StringType.TIGHT.value, StringType.DECODED.value])
if args.analyze_functions:
@ -285,7 +288,6 @@ def main(argv=None) -> int:
large_file=args.large_file,
quiet=args.quiet,
verbose=args.verbose,
no_prompt=args.no_prompt,
cache_dir=cache_dir,
)

View file

@ -105,9 +105,6 @@ class Options:
large_file: bool = False
quiet: bool = False
verbose: int = Verbosity.DEFAULT
# when True, do not prompt on TTY for deobfuscation on language binaries;
# deobfuscation then defaults to not running
no_prompt: bool = False
# analysis cache directory; None disables caching (default in the CLI is
# the platform cache directory via floss.cache.get_cache_dir())
cache_dir: Optional[Path] = None
@ -390,41 +387,6 @@ def analyze(options: Options) -> Optional[ResultDocument]:
analysis.enable_tight_strings = False
analysis.enable_decoded_strings = False
enabled_string_types = options.enabled_string_types or []
disabled_string_types = options.disabled_string_types or []
if results.metadata.language not in ("", "unknown"):
if not enabled_string_types and not disabled_string_types:
if not options.no_prompt:
# when stdout is redirected, such as in 'floss foo.exe | less' use default prompt values
if sys.stdout.isatty():
try:
prompt = input(
"Do you want to enable string deobfuscation? (this could take a long time) [y/N] "
)
except KeyboardInterrupt:
raise PipelineError("aborted by user", exit_code=130)
except EOFError:
raise PipelineError("aborted by user", exit_code=1)
else:
prompt = "n"
if prompt.lower() == "y":
logger.info("enabled string deobfuscation")
analysis.enable_stack_strings = True
analysis.enable_tight_strings = True
analysis.enable_decoded_strings = True
else:
logger.info("disabled string deobfuscation")
analysis.enable_stack_strings = False
analysis.enable_tight_strings = False
analysis.enable_decoded_strings = False
else:
# --no-prompt: never prompt, default to not running deobfuscation
logger.info("string deobfuscation disabled (--no-prompt)")
analysis.enable_stack_strings = False
analysis.enable_tight_strings = False
analysis.enable_decoded_strings = False
# in order of expected run time, fast to slow
# 1. static strings (done above for language ID; layout-aware replace below when enabled)
# a) includes language-specific strings, if applicable

View file

@ -39,8 +39,9 @@ def test_shellcode(scfile):
assert floss.main.main([scfile, "-f", "sc32"]) == 0
assert floss.main.main([scfile, "--format", "sc64"]) == 0
# fail
assert floss.main.main([scfile, "--format", "pe"]) == -1
# fail: forcing the PE format on shellcode only errors once deobfuscation
# (which runs vivisect) is requested
assert floss.main.main([scfile, "--format", "pe", "--string-type", "stack", "tight", "decoded"]) == -1
@pytest.mark.parametrize("type_", [t.value for t in StringType])
@ -65,14 +66,14 @@ def test_args_analysis_type_conflict(exefile):
def test_language_extraction_independent_of_static(capsys):
"""language strings are extracted even when static strings are disabled.
uses --string-type language (so only language extraction runs) and --no-prompt so the
deobfuscation prompt is skipped, on a Go sample whose language is detectable.
uses --string-type language (so only language extraction runs) on a Go sample
whose language is detectable, on a Go sample whose language is detectable.
"""
import json
sample = Path(__file__).parent / "data" / "language" / "go" / "go-hello" / "bin" / "go-hello64.exe"
assert floss.main.main([str(sample), "--string-type", "language", "--no-prompt", "-j"]) == 0
assert floss.main.main([str(sample), "--string-type", "language", "-j"]) == 0
doc = json.loads(capsys.readouterr().out)
assert doc["metadata"]["language"] == "go"
assert len(doc["strings"]["language_strings"]) > 0
@ -86,7 +87,7 @@ def test_manual_language_override_wins_over_auto_detect(capsys):
# a C binary, so auto-detection yields unknown; forcing go must stick
sample = Path(__file__).parent / "data" / "src" / "decode-in-place" / "bin" / "test-decode-in-place.exe"
assert floss.main.main([str(sample), "--language", "go", "--string-type", "language", "--no-prompt", "-j"]) == 0
assert floss.main.main([str(sample), "--language", "go", "--string-type", "language", "-j"]) == 0
doc = json.loads(capsys.readouterr().out)
assert doc["metadata"]["language"] == "go"
assert doc["metadata"]["language_selected"] == "go"
@ -106,7 +107,7 @@ def test_manual_language_override_beats_wrong_auto_detect(monkeypatch, capsys):
monkeypatch.setattr(floss.language.identify, "identify_language_and_version", fake_identify)
sample = Path(__file__).parent / "data" / "src" / "decode-in-place" / "bin" / "test-decode-in-place.exe"
assert floss.main.main([str(sample), "--language", "go", "--string-type", "language", "--no-prompt", "-j"]) == 0
assert floss.main.main([str(sample), "--language", "go", "--string-type", "language", "-j"]) == 0
doc = json.loads(capsys.readouterr().out)
assert doc["metadata"]["language"] == "go"
assert doc["metadata"]["language_version"] == ""
@ -139,7 +140,6 @@ def test_no_layout_yields_classic_static(exefile):
enable_layout=False,
enable_tags=True,
),
no_prompt=True,
)
)
assert results is not None
@ -169,7 +169,6 @@ def test_no_tags_skips_tag_databases(exefile):
enable_layout=True,
enable_tags=False,
),
no_prompt=True,
)
)
assert results is not None
@ -193,15 +192,16 @@ def test_no_tags_skips_tag_databases(exefile):
assert db_tag not in s.tags
def test_no_prompt_skips_deobfuscation_for_go(capsys):
"""--no-prompt on a Go sample defaults to not running deobfuscation."""
def test_deobfuscation_off_by_default(capsys):
"""deobfuscation is opt-in: a plain run leaves stack/tight/decoded off."""
import json
sample = Path(__file__).parent / "data" / "language" / "go" / "go-hello" / "bin" / "go-hello64.exe"
assert floss.main.main([str(sample), "--no-prompt", "-j"]) == 0
assert floss.main.main([str(sample), "-j"]) == 0
doc = json.loads(capsys.readouterr().out)
assert doc["metadata"]["language"] == "go"
assert doc["analysis"]["enable_static_strings"] is True
assert doc["analysis"]["enable_stack_strings"] is False
assert doc["analysis"]["enable_tight_strings"] is False
assert doc["analysis"]["enable_decoded_strings"] is False