Commit graph

385 commits

Author SHA1 Message Date
Tushar Umbarkar
112f6cf5ce
feat: add snprintf/sprintf hook (#1210)
* feat: add snprintf/sprintf hook

* test: add unit tests for SnprintfHook

* fix: resolve isort lint errors in tests

* fix: resolve black formatting and mypy type errors
2026-08-27 09:41:10 +02:00
aa1c5398a4
tests: cover tcsh and powershell completion output 2026-08-26 16:39:38 +05:30
7b1f719215
cli: add shell completion scripts (--print-completion)
generate bash/zsh/tcsh/fish/powershell completion scripts from the
argparse parser via shtab, so completions always match the CLI args

- floss --print-completion {bash,zsh,...} emits an installable script
- the sample positional completes file paths
- choice values (--string-type, --format, --language, --columns)
  complete automatically from argparse definitions
- document per-shell installation in doc/usage.md

fixes #1350
2026-08-25 20:04:53 +05:30
0e97a1f6fd
test(oss): drop rebuild-dependent zlib metadata asserts
The OSS database rebuild changes build-specific fields such as
file_path (e.g. CMakeFiles/zlibstatic.dir/inffast.c.obj). Drop the
hardcoded file_path and function_name asserts and keep only stable
metadata.
2026-08-20 01:22:46 +05:30
eaad64612d
test(oss): drop hardcoded zlib version assert
The OSS database rebuild bumps zlib to 1.3.2#2, so asserting an exact
library version in the test is brittle. Drop the hardcoded version check.
2026-08-20 01:07:02 +05:30
461113d04f
test(cli): drop deobfuscation-on-by-default test
It only re-asserted the Analysis defaults and added a slow full run; the
default all-on behavior is covered implicitly elsewhere.
2026-08-19 17:49:51 +05:30
7bdd3fe9c4
test(cli): fix duplicated language-test docstring 2026-08-19 17:47:55 +05:30
864e689552
refactor(cli): reject any string-type selection with --summary
Previously only non-static values were rejected, silently allowing
--summary --string-type static. --summary is its own static-only view, so
any --string-type/--no-string-type is now an argument error.
2026-08-19 17:46:22 +05:30
3569962ff3
test(cache): rely on the compute_key auto default
format='auto' is now the default, so tests no longer pass it explicitly.
2026-08-19 17:45:30 +05:30
bfc01b026a
revert(cli): restore all-on default string extraction
Undo the opt-in default from 0abe0c2 (default now runs all string types,
deobfuscation included) per PR #1376 discussion — the final behavior is
still being debated. The interactive deobfuscation prompt stays removed.
The --summary static-only shortcut and --analyze-functions are unchanged.
2026-08-19 15:24:09 +05:30
0abe0c2103
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.
2026-08-19 15:13:21 +05:30
f700666757
refactor(cache): default the analysis format in compute_key
compute_key(sha256, version, format='auto') so callers that only need the
default variant don't have to pass it explicitly.
2026-08-19 15:06:51 +05:30
8c2451c5d1
refactor(cli): rename -y/--yes to --no-prompt
--yes implied it enables deobfuscation, but the flag skips the prompt and
defaults to not running it, which confused reviewers. --no-prompt states
the intent. The dest and Options field become no_prompt (store_true).
2026-08-19 11:51:56 +05:30
26af97d049
style: black/isort formatting for earlier fixes 2026-08-19 04:51:04 +05:30
7c4d4d02a8
fix(tags): detect unpulled Git LFS pointers in all loaders
Only gp.StringHashDatabase checked for a Git LFS pointer; the oss, winapi,
and expert loaders failed with a confusing BadGzipFile/msgspec error on a
fresh clone without git lfs pull. A shared ensure_not_lfs_pointer() helper
now raises the actionable message, and gp's loader is refactored onto it.
2026-08-19 04:50:45 +05:30
552b48755a
fix(cli): reject --summary with --analyze-functions
--summary auto-disables stack/tight/decoded and --analyze-functions
disables static, so the combination silently left every string type
disabled and rendered an empty summary. Since the summary only covers
static strings, which --analyze-functions cannot show, the two are
incompatible and now raise a clear argument error.
2026-08-19 04:48:05 +05:30
13c3fc8dcd
fix(layout): annotate PE/ELF root-level strings with structures
PELayout/ELFLayout mark_structures overrides only recursed into children,
never applying structures_by_address to their own strings, so strings in
the ELF header/program-header gap (attached to the root node) and PE
header-gap strings rendered with an empty .structure. Both now annotate
their own strings before recursing, matching MachOLayout.
2026-08-19 04:47:06 +05:30
47445d6a6e
fix(cache): include the analysis format in the cache key
The key was sha256+version only, so interpreting the same bytes under a
different --format (sc32 vs sc64) could serve the other variant's
stack/tight/decoded results. The CLI already disables caching for
non-auto formats, but API callers passing cache_dir were unprotected.
Format is now part of the key, so variants never collide.
2026-08-19 04:44:01 +05:30
916e310e8a
fix(pipeline): -y/--yes now disables deobfuscation
The help text says -y defaults to not running deobfuscation, but the
prompt branch -y bypasses was the only place stack/tight/decoded got
disabled, so a non-interactive Go/Rust run with -y ran full deobfuscation
anyway (only .NET was force-disabled). -y now skips the prompt and
disables deobfuscation, matching the documented default.
2026-08-19 04:42:18 +05:30
21a9a71c4c
fix(enrich): recognize XOR-decoded PE/ELF layout names
XOR-obfuscated headers append ' (XOR decoded with key: 0x...)' to the
layout name, so is_structured_layout()'s exact 'pe'/'elf' match failed
and compute_layout() silently fell back to classic static strings,
losing section names, code/reloc/XOR tags, and structure annotations.
Strip any parenthetical suffix before matching.
2026-08-19 04:39:54 +05:30
Moritz
0471fb106e
Merge pull request #1366 from mandiant/feat/render-summary-layout
feat(render): aesthetically revamp summary mode layout and hierarchy
2026-08-18 19:41:19 +02:00
mr-tz
80588667b9 feat(render): aesthetically revamp summary mode layout and tag filtering
This PR fundamentally revamps the terminal summary display mode for clearer navigation and density.

Architectural Improvements:
- Integrates `interesting` strings inline directly beneath their exact binary sections in the structural map.
- Strictly aligns summary rendering boundaries to precisely mirror the main FLOSS display spacing and aesthetic (e.g. gray offsets).
- Enforces a 5-string max sampling quota per section to maintain strict summary conciseness without overwhelming the console layout on large binaries.
- Establishes a clean visual section separation utilizing explicitly bracketed headers and empty newlines, entirely stripping out extraneous table headers and placeholders.
- Unified the concept of "interesting strings" across both the LayoutFilter (--interesting) parser and the summary output. We now correctly drop strings mapped purely to noisy tags (#code, #common), but successfully rescue any strings invoking highlight rules (e.g. valid #capa rules hitting code intersections).

TAG=agy
2026-08-18 17:50:53 +02:00
0db76156ad
fix(cache): guard is_file() in load() against OSError
load() probed the cache entry with path.is_file() unguarded, but on
Python 3.8+ PermissionError from stat() through a locked cache directory
is not swallowed (only ENOENT-like errnos are), so it crashed the
analysis instead of treating the entry as a miss.
2026-08-18 17:33:36 +05:30
408302d3d4
fix(cache): parse boolean envars case-insensitively
FLOSS_CACHE_ENABLE=False or FLOSS_CACHE_REFRESH=True were evaluated
verbatim, so the uppercase variants silently did nothing. Normalize the
value with .lower() so 0/false/no/n and 1/true/yes/y work regardless of
capitalization.
2026-08-18 16:46:54 +05:30
82293a3131
fix(cache): record the requested min_length on cache hits
materialize() filtered the document to the requested -n but left
metadata.min_length at the cached extraction threshold, so --json on a
hit (e.g. -n 6 against a -n 4 entry) advertised min_length: 4. Set it to
the requested value, matching what load() does for user-supplied
documents and what a fresh run reports.
2026-08-18 16:46:28 +05:30
638f1f50a5
fix(cache): never crash on cache init failures
cache_dir.mkdir() in store() and tempfile.mkstemp() in _write_atomic()
ran outside the OSError guard: an unwritable cache directory or a full
disk raised straight through and aborted the analysis. Both now log a
warning and skip caching.
2026-08-18 16:46:10 +05:30
a5e7aa0097
feat(cache): add FLOSS_CACHE_REFRESH to force a re-analysis
Deleting a cache entry manually is the only forced-refresh path today, and
FLOSS_CACHE_ENABLE=0 disables reads and writes entirely. FLOSS_CACHE_REFRESH=1
bypasses the cache on the current run and overwrites the entry with the
fresh document, documented in --help alongside the other envars.
2026-08-18 14:15:28 +05:30
7a9b529100
test(cache): narrow optional layout before access for mypy
the new materialize tag tests asserted layout.strings without proving the
layout is present, failing the CI mypy check. assert layout is not None
first so the union narrows.
2026-08-18 01:45:38 +05:30
1571c403e0
fix(cache): let a tags-enabled cache serve a no-tags request
covers() treated surplus tag data as a miss: if not wanted.enable_tags and
cached.analysis.enable_tags was a miss, forcing a full re-analysis just
because the cache holds more than requested. Tag false-positive cleanup
(remove_false_positive_lib_strings) only redacts tags, never strings, so
the cached data is a faithful superset.

covers() now accepts it, and materialize() redacts the tags across the
static, language, and layout-tree strings when tags are not wanted.
2026-08-18 01:31:00 +05:30
f516fe22b8
fix(cache): never crash analysis on cache file I/O errors
load() removed stale entries with path.unlink() and store() replaced the
entry with os.replace(), neither guarded. On Windows a cache file held
open by another process or an antivirus scanner raises PermissionError,
which propagated and aborted the analysis.

Writes and removals are now best-effort: _write_atomic() logs and skips
on any OSError during mkstemp/write/replace and cleans up the temporary
file, and _drop_cache_entry() logs and continues when an entry cannot be
removed.
2026-08-18 01:28:48 +05:30
82e544668b
fix(cache): clear disabled string types on cache materialize
A cache hit for a document that holds more string types than the user
requested previously left those arrays populated: covers() treats a
superset as a hit, but materialize() only flipped the analysis flags.
With --json the output then contained data a fresh run would omit.

materialize() now mirrors a fresh run: disabled types are cleared from
the document (static also drops the layout that holds them) and the
enable_layout/enable_tags flags are synced to the requested analysis.
2026-08-18 01:28:14 +05:30
bc94b6cb27
fix(cache): allow layout-less cache hits and clarify miss comment
covers() no longer treats a cached layout as a miss for a no-layout
request: materialize() now drops the layout when it is not wanted. Tags
remain a miss because a tags-enabled document has already had
false-positive strings removed. Move the --analyze-functions comment
inline with the caching guard it documents.
2026-08-18 01:06:59 +05:30
fba96b0fb6
fix(cache): make lock and cache-dir tests Windows-compatible
msvcrt.locking cannot lock a byte range past EOF, so the lock on an
empty lock file always failed on Windows and store() never wrote a cache
entry. Ensure the lock file has at least one byte before locking.

test_get_cache_dir_default asserted the default path ends with 'floss',
but the Windows default is %LOCALAPPDATA%\floss\Cache. Assert it
equals platformdirs' user_cache_dir('floss') instead.
2026-08-18 00:19:12 +05:30
199de0f7ca
test(cache): isolate cache dir and add end-to-end cache tests
Add an autouse fixture that points FLOSS_CACHE_DIR at a throwaway
directory per test so the suite never touches the real platform cache.
Add integration tests for a cache hit (identical output, hit surfaced
via -d), FLOSS_CACHE_ENABLE=0, no caching for user-supplied JSON result
documents, and non-default analysis variants (explicit --format).
2026-08-17 19:06:46 +05:30
d8a4fe8bd9
feat(cache): add result-document cache module
Add floss/cache.py with cache directory resolution, content-addressed
key computation, validated load, and an atomic lock-guarded store. The
module also decides whether a cached document satisfies the requested
analysis (covers) and applies post-load filtering (materialize).

Unit tests in tests/test_cache.py cover enable switch, directory
override, key/path layout, store/load roundtrip, invalid-entry dropping,
lock contention, coverage checks, and materialize filtering.
2026-08-17 18:45:48 +05:30
f3263ff186
fix(results): abort when -n is below the min length used to build a doc
Loading a saved results document with a --minimum-length below the value used
at extraction silently drops strings that were already gone at extraction time
and cannot be recovered. Instead of just warning, abort with InvalidLoadConfig
so the user is not misled into thinking the smaller threshold applies.
2026-08-17 16:34:06 +05:30
e1bc7f9f83
test(render): add tag-normalization, Mach-O summary, no-layout warning tests
Remove two redundant tests: test_filter_tag_family_winapi duplicated
test_filter_by_tag (the winapi family is a single-element set, so the family
and direct-tag paths are indistinguishable), and test_main_columns_accumulate
duplicated test_main_columns_flag and would pass even if accumulation broke.

Add coverage for previously untested branches:
- test_filter_tag_normalization: #/case-insensitive tag matching
- test_summary_section_counts_thread_fat_macho: analyze_layout Mach-O
  fat-arch section threading
- test_layout_none_warns_filters_ignored: the warning emitted when an
  active layout filter has no layout tree to apply to
2026-08-17 15:40:02 +05:30
f73c7efcbe
fix(cli): reject non-static string types with --summary
--summary only covers static strings, so a --string-type/--no-string-type
selection for stack/tight/decoded/language is a shadow arg that does nothing.
Reject it at parse time instead of silently ignoring it; --summary alone (or
with an explicit static selection) still works and skips the slow
deobfuscation for the recovered string types.
2026-08-17 15:12:57 +05:30
03ec9e9396
test(cli): route language-override tests through floss.main with -y
Now that -y suppresses the deobfuscation prompt, the manual --language override
tests can exercise the real floss.main entry point via JSON output instead of
calling the pipeline directly.
2026-08-17 14:55:15 +05:30
4731c8d0f9
test(cli): use -y and floss.main for the language extraction test
Drives floss.main with --string-type language and the new -y flag instead of
calling the pipeline directly to dodge the deobfuscation prompt, so the test
exercises the real entry point and the prompt-suppression flag it relies on.
2026-08-17 14:52:48 +05:30
cac88eb79d
test(cli): remove -- terminator filter-flag test
The multi-value flags needing a -- before the sample is a documented argparse
convention; locking it in with a test is not worth maintaining.
2026-08-17 14:52:32 +05:30
8ef8bc10b6
test(cli): remove --language auto acceptance test
--language auto is the default and there's no value in a spot-check test for
one of many possible choices; keeping it invites testing every type and arg.
2026-08-17 14:52:25 +05:30
cc66e7ebd5
fix(results): warn when -n is below the min length used to build a doc
Loading a saved results document with a --minimum-length below the value used
at extraction silently drops any shorter strings, but those strings were
already gone at extraction time and cannot be recovered. Warn so users aren't
misled into thinking the smaller threshold applies.
2026-08-17 14:51:48 +05:30
9fac300a31
fix(render): restore classic meta fallback when static strings are disabled
Loading a saved results document and disabling static strings (--no-string-type
static) previously dropped the entire metadata table, because the layout branch
matched on layout presence alone and the inner static-enabled check swallowed
the output. When a layout exists but static strings are disabled, fall back to
the classic metadata view (file path, hashes, language) instead of rendering
nothing. Also warn when layout-aware filters are active but no layout tree
exists, so --section/--tag/--query/--max-strings/--interesting don't silently
no-op on unsupported formats.
2026-08-16 15:18:12 +05:30
968c1953c7
test(cli): remove spurious --language dotnet rejection test 2026-08-16 15:03:52 +05:30
83f5788d32
fix(cli): reject --language dotnet; --interesting drops any noisy-tag string
- --language choices are now auto/go/rust/none per spec 3.4.7; dotnet is
  rejected because .NET extraction is unimplemented
- --interesting now drops any string carrying a noisy tag, even alongside a
  non-noisy tag (#winapi #common is dropped), matching the spec's 'excludes
  the noisy tags' wording
2026-08-16 14:13:00 +05:30
d737ef6a7e
fix(pipeline): a concrete --language selection unilaterally beats auto-detection
The earlier guard only honored a manual --language go/rust when auto-detection
returned unknown. When auto-detection wrongly identified a different language
(e.g. rust), the manual override was ignored and the auto-detected language was
used. A concrete manual selection now always wins; the language version is kept
only when it matches the selection. Also make the missing-file DB test robust
by using tmp_path instead of a hardcoded /nonexistent path (which raises
PermissionError on some Linux CI environments).
2026-08-15 02:42:50 +05:30
dbfa8702cc
feat(summary): --summary is static-only by default
The summary's layout-derived sections (section counts, tag histogram,
high-value strings) cover static strings only, but --summary still spun up the
slow stack/tight/decoded deobfuscation. When --summary is requested and no
string types were explicitly selected, disable the recovered string types;
explicit --string-type/--no-string-type selection overrides the default.
2026-08-15 01:40:46 +05:30
8b97bfd93e
feat(json): keep extra top-level keys instead of dropping them
render() only emitted the known TOP_LEVEL_KEYS, so a future document with an
additional top-level field would silently lose it. Append any extra keys after
the fixed ordering (metadata first), preserving detect_file_type behavior.
Add tests for the extra-key guard and the manual --language override.
2026-08-14 20:32:24 +05:30
6c0ef2fa5d
feat(cli): --columns accumulates across repeated flags
--columns used store (last wins) while the other filter flags extend
(accumulate). Switch to extend with an empty default and fall back to
DEFAULT_COLUMNS in render when nothing was passed, so repeated --columns
flags accumulate and the default behavior is unchanged.
2026-08-14 20:32:23 +05:30