narvy-cli 1.1.1__tar.gz → 1.1.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/CHANGELOG.md +84 -0
- {narvy_cli-1.1.1/narvy_cli.egg-info → narvy_cli-1.1.3}/PKG-INFO +79 -4
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/README.md +78 -3
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/narvy/__init__.py +1 -1
- narvy_cli-1.1.3/narvy/__main__.py +30 -0
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/narvy/apk_memory_preflight.py +78 -16
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/narvy/decompiler.py +48 -15
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/narvy/doctor.py +1 -1
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/narvy/host/lynis_bootstrap.py +1 -1
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/narvy/host/ssh_exec.py +2 -2
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/narvy/ios/binary_analyzer.py +2 -2
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/narvy/main.py +21 -3
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/narvy/native_hardening.py +1 -1
- narvy_cli-1.1.3/narvy/pathhint.py +98 -0
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/narvy/proc.py +4 -0
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/narvy/rules/web/go.yml +132 -50
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/narvy/rules/web/java.yml +20 -0
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/narvy/rules/web/javascript.yml +597 -270
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/narvy/rules/web/php.yml +108 -32
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/narvy/rules/web/python.yml +108 -25
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/narvy/rules/web/ruby.yml +89 -77
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/narvy/rules/web/secrets.yml +6 -4
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/narvy/sca/web_deps.py +98 -16
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/narvy/semgrep_engine.py +112 -22
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/narvy/web/nuclei_binary.py +1 -1
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/narvy/web/scanner.py +1 -1
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/narvy/web/source_analyzer.py +74 -13
- {narvy_cli-1.1.1 → narvy_cli-1.1.3/narvy_cli.egg-info}/PKG-INFO +79 -4
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/narvy_cli.egg-info/SOURCES.txt +2 -0
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/LICENSE +0 -0
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/MANIFEST.in +0 -0
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/SECURITY.md +0 -0
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/narvy/android/__init__.py +0 -0
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/narvy/android/source_analyzer.py +0 -0
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/narvy/android/split_bundle.py +0 -0
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/narvy/android_rule_context.py +0 -0
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/narvy/auth.py +0 -0
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/narvy/ci_templates/bitbucket.yml +0 -0
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/narvy/ci_templates/github.yml +0 -0
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/narvy/ci_templates/gitlab.yml +0 -0
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/narvy/cloud/__init__.py +0 -0
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/narvy/cloud/aws_scan.py +0 -0
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/narvy/comment_filter.py +0 -0
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/narvy/crypto_taint_lite.py +0 -0
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/narvy/host/__init__.py +0 -0
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/narvy/host/audit.py +0 -0
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/narvy/host/host_knowledge.py +0 -0
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/narvy/host/lynis_parser.py +0 -0
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/narvy/host/narvy_checks.py +0 -0
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/narvy/host/report.py +0 -0
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/narvy/ios/__init__.py +0 -0
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/narvy/ios/plist_checks.py +0 -0
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/narvy/ios/source_analyzer.py +0 -0
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/narvy/ios/third_party_filter.py +0 -0
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/narvy/ios/trust_all_context.py +0 -0
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/narvy/own_reports.py +0 -0
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/narvy/reporter.py +0 -0
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/narvy/rule_engine.py +0 -0
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/narvy/rules/android/config.yml +0 -0
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/narvy/rules/android/crypto.yml +0 -0
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/narvy/rules/android/secrets.yml +0 -0
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/narvy/rules/android/storage.yml +0 -0
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/narvy/rules/android/webview.yml +0 -0
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/narvy/rules/android.yml +0 -0
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/narvy/rules/ios/objc/crypto.yml +0 -0
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/narvy/rules/ios/objc/network.yml +0 -0
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/narvy/rules/ios/objc/secrets.yml +0 -0
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/narvy/rules/ios/objc/storage.yml +0 -0
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/narvy/rules/ios/objc/webview.yml +0 -0
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/narvy/rules/ios_swift.yml +0 -0
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/narvy/rules/web/kotlin.yml +0 -0
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/narvy/rules/web/rust.yml +0 -0
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/narvy/rules/web/secrets_supplement.yml +0 -0
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/narvy/sca/__init__.py +0 -0
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/narvy/sca/android_deps.py +0 -0
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/narvy/sca/ios_deps.py +0 -0
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/narvy/sca/osv_client.py +0 -0
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/narvy/scope_config.py +0 -0
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/narvy/stack_protector_evidence.py +0 -0
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/narvy/telemetry.py +0 -0
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/narvy/third_party_filter.py +0 -0
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/narvy/uploader.py +0 -0
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/narvy/weak_prng_context.py +0 -0
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/narvy/web/__init__.py +0 -0
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/narvy/web/scan_blocklist.py +0 -0
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/narvy/web/ssrf_guard.py +0 -0
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/narvy_cli.egg-info/dependency_links.txt +0 -0
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/narvy_cli.egg-info/entry_points.txt +0 -0
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/narvy_cli.egg-info/requires.txt +0 -0
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/narvy_cli.egg-info/top_level.txt +0 -0
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/pyproject.toml +0 -0
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/setup.cfg +0 -0
- {narvy_cli-1.1.1 → narvy_cli-1.1.3}/setup.py +0 -0
|
@@ -5,6 +5,90 @@ All notable changes to this project are documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.1.3]
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- APK scans were refused before decompiling on machines with less free
|
|
13
|
+
memory than `--max-mem` needs (about 1.3x the heap plus 1 GB), even for
|
|
14
|
+
small apps. On an 8 GB laptop at the default `--max-mem 4g` that was
|
|
15
|
+
nearly every app, and on macOS the free-memory reading was low by
|
|
16
|
+
several GB. `--max-mem` is a ceiling: when it does not fit but a smaller
|
|
17
|
+
heap still covers the app's estimated need by 1.5x, jadx now runs with
|
|
18
|
+
that smaller heap and the scan says so. A machine that cannot hold even
|
|
19
|
+
that is still refused, with the same advice as before.
|
|
20
|
+
- macOS: available memory comes from the kernel's own figure
|
|
21
|
+
(`kern.memorystatus_level`, what `memory_pressure` reports), which counts
|
|
22
|
+
memory the compressor can reclaim. The `vm_stat` page count is the
|
|
23
|
+
fallback and now includes speculative pages.
|
|
24
|
+
- APK scans: a structural (Semgrep) pass that failed was reported as
|
|
25
|
+
complete with 0 findings when semgrep exited with an error but still
|
|
26
|
+
printed JSON. It is now reported as incomplete with the error, after one
|
|
27
|
+
retry with a single worker. A run whose path filters matched none of the
|
|
28
|
+
app's own source files is reported the same way instead of as a clean
|
|
29
|
+
pass. Rule ids no longer depend on where the package is installed.
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
|
|
33
|
+
- Platform CI: the check removes the user Scripts/bin directory from PATH
|
|
34
|
+
before running scans (hosted runners ship with it on PATH, a new user's
|
|
35
|
+
shell does not), and prints the APK scan output on success so the
|
|
36
|
+
structural-pass status is in the log.
|
|
37
|
+
|
|
38
|
+
## [1.1.2]
|
|
39
|
+
|
|
40
|
+
### Added
|
|
41
|
+
|
|
42
|
+
- `python -m narvy` (`py -m narvy` on Windows) runs the CLI. It works when
|
|
43
|
+
pip put the `narvy` command in a directory that is not on PATH, the
|
|
44
|
+
default for a user install on Windows and macOS. Run that way, Narvy
|
|
45
|
+
prints once the command that adds the directory to PATH (PowerShell on
|
|
46
|
+
Windows, your shell rc file elsewhere). It never changes PATH itself;
|
|
47
|
+
`NARVY_NO_PATH_HINT=1` turns the hint off.
|
|
48
|
+
- README: install steps for Windows, macOS (Homebrew
|
|
49
|
+
`externally-managed-environment`) and Linux, with pipx first.
|
|
50
|
+
|
|
51
|
+
### Fixed
|
|
52
|
+
|
|
53
|
+
- A user install run as `python -m narvy` (or by the launcher's full path)
|
|
54
|
+
did not find semgrep in the same off-PATH directory, skipped the code pass
|
|
55
|
+
and reported only dependency CVEs. semgrep is now looked up there and its
|
|
56
|
+
directory is put on the PATH it runs with (it starts `pysemgrep` through
|
|
57
|
+
PATH).
|
|
58
|
+
- macOS: when no Java 11+ was installed, the downloaded JRE was never found
|
|
59
|
+
("no bin/ dir found inside"), so APK scans failed on every such Mac. The
|
|
60
|
+
macOS layout (`Contents/Home/bin`) is handled, Apple Silicon and ARM Linux
|
|
61
|
+
get the native aarch64 JRE instead of x64, and a cached JRE that does not
|
|
62
|
+
run is downloaded again.
|
|
63
|
+
- Windows: jadx failed to start when its path contained a space (a user name
|
|
64
|
+
like `Jean Dupont`, or `C:\Program Files`), because `cmd /c` stripped the
|
|
65
|
+
wrong quotes. The jadx command line now goes through `cmd /d /s /c` as one
|
|
66
|
+
quoted string.
|
|
67
|
+
- Windows: tool output (semgrep, jadx, nuclei, ssh) is decoded as UTF-8
|
|
68
|
+
instead of the console code page, semgrep runs in Python UTF-8 mode, and
|
|
69
|
+
output redirected to a file or pipe replaces a character the code page
|
|
70
|
+
cannot encode instead of stopping the scan with `UnicodeEncodeError`.
|
|
71
|
+
- A file still locked in a temporary directory (antivirus, a slow child
|
|
72
|
+
process) no longer fails the scan at cleanup.
|
|
73
|
+
- SCA: only one version of each package was checked. A lockfile that holds
|
|
74
|
+
`minimist 1.2.8` and a nested `minimist 0.0.8` now checks both. A version
|
|
75
|
+
taken from a manifest range is still dropped when a lockfile resolves that
|
|
76
|
+
package.
|
|
77
|
+
- yarn.lock (berry): the package comes from the `resolution:` line, so
|
|
78
|
+
aliases (`string-width-cjs@npm:string-width@...`) and `patch:` entries
|
|
79
|
+
name the real package, and `__metadata`, workspace, git and local entries
|
|
80
|
+
are no longer sent to OSV as packages.
|
|
81
|
+
- semgrep with more than one worker drops taint-mode results at random with
|
|
82
|
+
no error. Every rule pack that holds a taint rule now runs in its own
|
|
83
|
+
single-worker semgrep process (packs still run side by side). Files where
|
|
84
|
+
a rule hit the per-file timeout are named in the scan notes.
|
|
85
|
+
|
|
86
|
+
### Changed
|
|
87
|
+
|
|
88
|
+
- Community rule precision round (PHP, JavaScript, Go, Java, Python, Ruby):
|
|
89
|
+
fewer findings on code that is not reachable by an attacker. Informational
|
|
90
|
+
rules are lowered to LOW with the reason.
|
|
91
|
+
|
|
8
92
|
## [1.1.1]
|
|
9
93
|
|
|
10
94
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: narvy-cli
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.3
|
|
4
4
|
Summary: Free, local SAST scanner for Android, iOS, and polyglot web/backend source - plus passive web, host-audit, and cloud-posture scanning.
|
|
5
5
|
Author: Narvy
|
|
6
6
|
License: Apache-2.0
|
|
@@ -63,11 +63,83 @@ exactly in [Telemetry](#telemetry), which you can turn off.
|
|
|
63
63
|
## Install
|
|
64
64
|
|
|
65
65
|
```bash
|
|
66
|
-
|
|
66
|
+
pipx install narvy-cli # recommended, puts `narvy` on PATH (run `pipx ensurepath` once)
|
|
67
|
+
pip install narvy-cli # or plain pip
|
|
67
68
|
```
|
|
68
69
|
|
|
69
|
-
|
|
70
|
-
|
|
70
|
+
If your shell says `narvy: command not found` / `The term 'narvy' is not
|
|
71
|
+
recognized`, pip installed the command into a directory that is not on your
|
|
72
|
+
PATH. `python -m narvy` always works (`py -m narvy` on Windows), and prints
|
|
73
|
+
once the command that adds that directory to PATH. Narvy never edits PATH
|
|
74
|
+
itself.
|
|
75
|
+
|
|
76
|
+
### Windows
|
|
77
|
+
|
|
78
|
+
Recommended, with [pipx](https://pipx.pypa.io):
|
|
79
|
+
|
|
80
|
+
```powershell
|
|
81
|
+
py -m pip install --user pipx
|
|
82
|
+
py -m pipx ensurepath # then close and reopen the terminal
|
|
83
|
+
pipx install narvy-cli
|
|
84
|
+
narvy scan app.apk
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Without pipx:
|
|
88
|
+
|
|
89
|
+
```powershell
|
|
90
|
+
py -m pip install narvy-cli
|
|
91
|
+
py -m narvy scan app.apk
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
To use the short `narvy` command after a plain `pip install`, add pip's
|
|
95
|
+
Scripts directory to PATH. For the current PowerShell window only
|
|
96
|
+
(Python 3.14 from python.org; change `Python314` to your version):
|
|
97
|
+
|
|
98
|
+
```powershell
|
|
99
|
+
$env:Path += ";$env:APPDATA\Python\Python314\Scripts"
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
To make it permanent for your user account, then open a new terminal:
|
|
103
|
+
|
|
104
|
+
```powershell
|
|
105
|
+
[Environment]::SetEnvironmentVariable("Path", [Environment]::GetEnvironmentVariable("Path", "User") + ";$env:APPDATA\Python\Python314\Scripts", "User")
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
The exact directory is in pip's `WARNING: The script narvy.exe is installed
|
|
109
|
+
in ... which is not on PATH` line, and in the hint `py -m narvy` prints. The
|
|
110
|
+
Microsoft Store Python uses a different one (under
|
|
111
|
+
`%LOCALAPPDATA%\Packages\PythonSoftwareFoundation.Python.3.x_...\LocalCache\local-packages`).
|
|
112
|
+
Windows on ARM: semgrep has no Windows ARM64 build, use x64 Python.
|
|
113
|
+
|
|
114
|
+
### macOS
|
|
115
|
+
|
|
116
|
+
Homebrew's Python refuses `pip install` outside a virtualenv
|
|
117
|
+
(`error: externally-managed-environment`). Use pipx:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
brew install pipx && pipx ensurepath # then open a new terminal
|
|
121
|
+
pipx install narvy-cli
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
or a virtualenv (`python3 -m venv ~/.venvs/narvy && ~/.venvs/narvy/bin/pip install narvy-cli`).
|
|
125
|
+
A `pip install --user` puts `narvy` in `~/Library/Python/3.x/bin`, which is
|
|
126
|
+
not on PATH: add it with
|
|
127
|
+
`echo 'export PATH="$HOME/Library/Python/3.14/bin:$PATH"' >> ~/.zshrc`, or run
|
|
128
|
+
`python3 -m narvy`.
|
|
129
|
+
|
|
130
|
+
### Linux
|
|
131
|
+
|
|
132
|
+
pipx (`sudo apt install pipx` / `dnf install pipx`, then `pipx ensurepath`) is
|
|
133
|
+
the simplest. Debian 12+ and Ubuntu 23.04+ refuse a system-wide `pip install`
|
|
134
|
+
the same way Homebrew does. A `pip install --user` puts `narvy` in
|
|
135
|
+
`~/.local/bin`; if that is not on PATH, add
|
|
136
|
+
`export PATH="$HOME/.local/bin:$PATH"` to `~/.bashrc`, or run
|
|
137
|
+
`python3 -m narvy`.
|
|
138
|
+
|
|
139
|
+
### Requirements
|
|
140
|
+
|
|
141
|
+
Android scans need Java 11+. If none is found, a JRE (and jadx) is
|
|
142
|
+
downloaded into `~/.narvy/tools` on the first APK scan. Everything else
|
|
71
143
|
is pure Python plus a couple of optional extras:
|
|
72
144
|
|
|
73
145
|
```bash
|
|
@@ -215,6 +287,9 @@ number of checks it did not run. It never hides a finding it did compute.
|
|
|
215
287
|
in its own output when it applies.
|
|
216
288
|
- Only one architecture's native libraries are checked in a multi-ABI
|
|
217
289
|
APK/split bundle.
|
|
290
|
+
- Windows: a decompiled file whose full path passes 260 characters is only
|
|
291
|
+
read when Windows long-path support is enabled
|
|
292
|
+
(`LongPathsEnabled`); a very deep package tree in `%TEMP%` can hit it.
|
|
218
293
|
|
|
219
294
|
See [SETUP.md](SETUP.md) for detailed install/troubleshooting steps and the
|
|
220
295
|
exact commands this README's examples were run against.
|
|
@@ -30,11 +30,83 @@ exactly in [Telemetry](#telemetry), which you can turn off.
|
|
|
30
30
|
## Install
|
|
31
31
|
|
|
32
32
|
```bash
|
|
33
|
-
|
|
33
|
+
pipx install narvy-cli # recommended, puts `narvy` on PATH (run `pipx ensurepath` once)
|
|
34
|
+
pip install narvy-cli # or plain pip
|
|
34
35
|
```
|
|
35
36
|
|
|
36
|
-
|
|
37
|
-
|
|
37
|
+
If your shell says `narvy: command not found` / `The term 'narvy' is not
|
|
38
|
+
recognized`, pip installed the command into a directory that is not on your
|
|
39
|
+
PATH. `python -m narvy` always works (`py -m narvy` on Windows), and prints
|
|
40
|
+
once the command that adds that directory to PATH. Narvy never edits PATH
|
|
41
|
+
itself.
|
|
42
|
+
|
|
43
|
+
### Windows
|
|
44
|
+
|
|
45
|
+
Recommended, with [pipx](https://pipx.pypa.io):
|
|
46
|
+
|
|
47
|
+
```powershell
|
|
48
|
+
py -m pip install --user pipx
|
|
49
|
+
py -m pipx ensurepath # then close and reopen the terminal
|
|
50
|
+
pipx install narvy-cli
|
|
51
|
+
narvy scan app.apk
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Without pipx:
|
|
55
|
+
|
|
56
|
+
```powershell
|
|
57
|
+
py -m pip install narvy-cli
|
|
58
|
+
py -m narvy scan app.apk
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
To use the short `narvy` command after a plain `pip install`, add pip's
|
|
62
|
+
Scripts directory to PATH. For the current PowerShell window only
|
|
63
|
+
(Python 3.14 from python.org; change `Python314` to your version):
|
|
64
|
+
|
|
65
|
+
```powershell
|
|
66
|
+
$env:Path += ";$env:APPDATA\Python\Python314\Scripts"
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
To make it permanent for your user account, then open a new terminal:
|
|
70
|
+
|
|
71
|
+
```powershell
|
|
72
|
+
[Environment]::SetEnvironmentVariable("Path", [Environment]::GetEnvironmentVariable("Path", "User") + ";$env:APPDATA\Python\Python314\Scripts", "User")
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The exact directory is in pip's `WARNING: The script narvy.exe is installed
|
|
76
|
+
in ... which is not on PATH` line, and in the hint `py -m narvy` prints. The
|
|
77
|
+
Microsoft Store Python uses a different one (under
|
|
78
|
+
`%LOCALAPPDATA%\Packages\PythonSoftwareFoundation.Python.3.x_...\LocalCache\local-packages`).
|
|
79
|
+
Windows on ARM: semgrep has no Windows ARM64 build, use x64 Python.
|
|
80
|
+
|
|
81
|
+
### macOS
|
|
82
|
+
|
|
83
|
+
Homebrew's Python refuses `pip install` outside a virtualenv
|
|
84
|
+
(`error: externally-managed-environment`). Use pipx:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
brew install pipx && pipx ensurepath # then open a new terminal
|
|
88
|
+
pipx install narvy-cli
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
or a virtualenv (`python3 -m venv ~/.venvs/narvy && ~/.venvs/narvy/bin/pip install narvy-cli`).
|
|
92
|
+
A `pip install --user` puts `narvy` in `~/Library/Python/3.x/bin`, which is
|
|
93
|
+
not on PATH: add it with
|
|
94
|
+
`echo 'export PATH="$HOME/Library/Python/3.14/bin:$PATH"' >> ~/.zshrc`, or run
|
|
95
|
+
`python3 -m narvy`.
|
|
96
|
+
|
|
97
|
+
### Linux
|
|
98
|
+
|
|
99
|
+
pipx (`sudo apt install pipx` / `dnf install pipx`, then `pipx ensurepath`) is
|
|
100
|
+
the simplest. Debian 12+ and Ubuntu 23.04+ refuse a system-wide `pip install`
|
|
101
|
+
the same way Homebrew does. A `pip install --user` puts `narvy` in
|
|
102
|
+
`~/.local/bin`; if that is not on PATH, add
|
|
103
|
+
`export PATH="$HOME/.local/bin:$PATH"` to `~/.bashrc`, or run
|
|
104
|
+
`python3 -m narvy`.
|
|
105
|
+
|
|
106
|
+
### Requirements
|
|
107
|
+
|
|
108
|
+
Android scans need Java 11+. If none is found, a JRE (and jadx) is
|
|
109
|
+
downloaded into `~/.narvy/tools` on the first APK scan. Everything else
|
|
38
110
|
is pure Python plus a couple of optional extras:
|
|
39
111
|
|
|
40
112
|
```bash
|
|
@@ -182,6 +254,9 @@ number of checks it did not run. It never hides a finding it did compute.
|
|
|
182
254
|
in its own output when it applies.
|
|
183
255
|
- Only one architecture's native libraries are checked in a multi-ABI
|
|
184
256
|
APK/split bundle.
|
|
257
|
+
- Windows: a decompiled file whose full path passes 260 characters is only
|
|
258
|
+
read when Windows long-path support is enabled
|
|
259
|
+
(`LongPathsEnabled`); a very deep package tree in `%TEMP%` can hit it.
|
|
185
260
|
|
|
186
261
|
See [SETUP.md](SETUP.md) for detailed install/troubleshooting steps and the
|
|
187
262
|
exact commands this README's examples were run against.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""`python -m narvy` (or `py -m narvy` on Windows) runs the CLI.
|
|
2
|
+
|
|
3
|
+
Works even when pip put the `narvy` command in a Scripts/bin directory that is
|
|
4
|
+
not on PATH, which is the default for a user install on Windows and macOS.
|
|
5
|
+
"""
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _prog() -> str:
|
|
11
|
+
"""Name shown in usage lines: the interpreter the user actually typed, plus -m narvy."""
|
|
12
|
+
argv0 = (getattr(sys, "orig_argv", None) or [""])[0]
|
|
13
|
+
name = os.path.basename(argv0)
|
|
14
|
+
if name.lower().endswith(".exe"):
|
|
15
|
+
name = name[:-4]
|
|
16
|
+
if not name.lower().startswith(("python", "py")):
|
|
17
|
+
name = "python"
|
|
18
|
+
return f"{name} -m narvy"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def main() -> None:
|
|
22
|
+
from .pathhint import maybe_print_path_hint
|
|
23
|
+
from .main import cli
|
|
24
|
+
|
|
25
|
+
maybe_print_path_hint()
|
|
26
|
+
cli(prog=_prog())
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
if __name__ == "__main__":
|
|
30
|
+
main()
|
|
@@ -24,6 +24,13 @@ JVM_RSS_OVERHEAD = 1.30
|
|
|
24
24
|
|
|
25
25
|
RAM_SAFETY_RESERVE_MB = 1024
|
|
26
26
|
|
|
27
|
+
# When --max-mem does not fit in free RAM but a smaller heap still leaves this
|
|
28
|
+
# much headroom over the estimate, jadx runs with that smaller -Xmx instead of
|
|
29
|
+
# the scan being refused. -Xmx bounds the JVM, so the smaller heap is what keeps
|
|
30
|
+
# memory in check. Measured: four real apps (6.5k to 60k classes) decompiled
|
|
31
|
+
# fully at 1.5x the estimate, peak RSS about 1.2x that heap.
|
|
32
|
+
AUTO_LOWER_MARGIN = 1.5
|
|
33
|
+
|
|
27
34
|
|
|
28
35
|
class DexStats(NamedTuple):
|
|
29
36
|
methods: int
|
|
@@ -41,6 +48,8 @@ class PreflightVerdict(NamedTuple):
|
|
|
41
48
|
max_mem_mb: int
|
|
42
49
|
available_mb: Optional[int]
|
|
43
50
|
stats: DexStats
|
|
51
|
+
# -Xmx value to run jadx with when it differs from --max-mem, else None.
|
|
52
|
+
lowered_max_mem: Optional[str] = None
|
|
44
53
|
|
|
45
54
|
|
|
46
55
|
def parse_mem_arg(value: str) -> Optional[int]:
|
|
@@ -71,19 +80,7 @@ def available_ram_mb() -> Optional[int]:
|
|
|
71
80
|
if line.startswith("MemAvailable:"):
|
|
72
81
|
return int(line.split()[1]) // 1024
|
|
73
82
|
elif system == "Darwin":
|
|
74
|
-
|
|
75
|
-
import subprocess
|
|
76
|
-
out = subprocess.run(["vm_stat"], capture_output=True, text=True, timeout=10).stdout
|
|
77
|
-
page = 4096
|
|
78
|
-
pm = re.search(r"page size of (\d+) bytes", out)
|
|
79
|
-
if pm:
|
|
80
|
-
page = int(pm.group(1))
|
|
81
|
-
counts = dict(re.findall(r"^(.+?):\s+(\d+)\.", out, re.M))
|
|
82
|
-
free = int(counts.get("Pages free", 0))
|
|
83
|
-
inactive = int(counts.get("Pages inactive", 0))
|
|
84
|
-
purgeable = int(counts.get("Pages purgeable", 0))
|
|
85
|
-
if free or inactive:
|
|
86
|
-
return ((free + inactive + purgeable) * page) // (1024 * 1024)
|
|
83
|
+
return _darwin_available_mb()
|
|
87
84
|
elif system == "Windows":
|
|
88
85
|
class _MemStatusEx(ctypes.Structure):
|
|
89
86
|
_fields_ = [
|
|
@@ -109,6 +106,58 @@ def available_ram_mb() -> Optional[int]:
|
|
|
109
106
|
return None
|
|
110
107
|
|
|
111
108
|
|
|
109
|
+
def _sysctl_int(name: str) -> Optional[int]:
|
|
110
|
+
import subprocess
|
|
111
|
+
try:
|
|
112
|
+
out = subprocess.run(["sysctl", "-n", name], capture_output=True, text=True,
|
|
113
|
+
encoding="utf-8", errors="replace", timeout=10).stdout.strip()
|
|
114
|
+
except (OSError, subprocess.SubprocessError):
|
|
115
|
+
return None
|
|
116
|
+
return int(out) if out.isdigit() else None
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _vm_stat_available_mb(out: str) -> Optional[int]:
|
|
120
|
+
"""free + inactive + speculative + purgeable pages from `vm_stat` output, in MB."""
|
|
121
|
+
page = 4096
|
|
122
|
+
pm = re.search(r"page size of (\d+) bytes", out)
|
|
123
|
+
if pm:
|
|
124
|
+
page = int(pm.group(1))
|
|
125
|
+
counts = dict(re.findall(r"^(.+?):\s+(\d+)\.", out, re.M))
|
|
126
|
+
pages = sum(int(counts.get(k, 0)) for k in
|
|
127
|
+
("Pages free", "Pages inactive", "Pages speculative", "Pages purgeable"))
|
|
128
|
+
if not pages:
|
|
129
|
+
return None
|
|
130
|
+
return (pages * page) // (1024 * 1024)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _darwin_available_mb(memsize: Optional[int] = None, level: Optional[int] = None,
|
|
134
|
+
vm_stat_out: Optional[str] = None) -> Optional[int]:
|
|
135
|
+
"""Available memory on macOS.
|
|
136
|
+
|
|
137
|
+
The kernel's own figure is kern.memorystatus_level (the "free percentage"
|
|
138
|
+
`memory_pressure` prints), which counts memory the compressor can reclaim.
|
|
139
|
+
Page counts from vm_stat miss that and read a few GB low on a machine that
|
|
140
|
+
is not under pressure at all, so they are only the fallback.
|
|
141
|
+
"""
|
|
142
|
+
import subprocess
|
|
143
|
+
if memsize is None:
|
|
144
|
+
memsize = _sysctl_int("hw.memsize")
|
|
145
|
+
if level is None:
|
|
146
|
+
level = _sysctl_int("kern.memorystatus_level")
|
|
147
|
+
from_level = None
|
|
148
|
+
if memsize and level is not None and 0 < level <= 100:
|
|
149
|
+
from_level = (memsize * level // 100) // (1024 * 1024)
|
|
150
|
+
if vm_stat_out is None:
|
|
151
|
+
try:
|
|
152
|
+
vm_stat_out = subprocess.run(["vm_stat"], capture_output=True, text=True,
|
|
153
|
+
encoding="utf-8", errors="replace", timeout=10).stdout
|
|
154
|
+
except (OSError, subprocess.SubprocessError):
|
|
155
|
+
vm_stat_out = ""
|
|
156
|
+
from_pages = _vm_stat_available_mb(vm_stat_out or "")
|
|
157
|
+
known = [v for v in (from_level, from_pages) if v is not None]
|
|
158
|
+
return max(known) if known else None
|
|
159
|
+
|
|
160
|
+
|
|
112
161
|
def _is_app_dex(name: str) -> bool:
|
|
113
162
|
"""True for app-code DEX (APK `classes*.dex` or App Bundle `<module>/dex/classes*.dex`), not asset .dex."""
|
|
114
163
|
base = name.rsplit("/", 1)[-1]
|
|
@@ -225,8 +274,20 @@ def check_memory_preflight(apk_path: str, max_mem: str,
|
|
|
225
274
|
|
|
226
275
|
if available_mb is not None:
|
|
227
276
|
needed_rss = int(max_mem_mb * JVM_RSS_OVERHEAD)
|
|
228
|
-
|
|
229
|
-
|
|
277
|
+
room_mb = available_mb - RAM_SAFETY_RESERVE_MB
|
|
278
|
+
if needed_rss > room_mb:
|
|
279
|
+
# -Xmx is a ceiling the JVM only grows to under pressure: when a smaller
|
|
280
|
+
# heap still fits this app with room to spare, use that instead of refusing.
|
|
281
|
+
fit_mb = (max(0, int(room_mb / JVM_RSS_OVERHEAD)) // 256) * 256
|
|
282
|
+
if fit_mb >= max(MIN_ESTIMATE_MB, need_mb * AUTO_LOWER_MARGIN):
|
|
283
|
+
note = (
|
|
284
|
+
f"Only {_fmt_mb(available_mb)} of memory is available right now, so JADX runs "
|
|
285
|
+
f"with a {_fmt_mb(fit_mb)} Java heap instead of --max-mem {max_mem} "
|
|
286
|
+
f"(this app needs about {_fmt_mb(need_mb)})."
|
|
287
|
+
)
|
|
288
|
+
return PreflightVerdict(False, "lowered", note, need_mb, max_mem_mb,
|
|
289
|
+
available_mb, stats, f"{fit_mb}m")
|
|
290
|
+
safe_gb = max(1, int(room_mb / JVM_RSS_OVERHEAD / 1024))
|
|
230
291
|
lower_line = f" - lower to --max-mem {safe_gb}g, which fits in what's free"
|
|
231
292
|
if safe_gb * 1024 < need_mb:
|
|
232
293
|
lower_line += (f" (though this app looks like it needs about {_fmt_mb(need_mb)}, "
|
|
@@ -234,7 +295,8 @@ def check_memory_preflight(apk_path: str, max_mem: str,
|
|
|
234
295
|
msg = (
|
|
235
296
|
f"--max-mem {max_mem} asks JADX for {_fmt_mb(max_mem_mb)} of Java heap, which needs "
|
|
236
297
|
f"about {_fmt_mb(needed_rss)} of real memory once JVM overhead is counted. This "
|
|
237
|
-
f"machine only has {_fmt_mb(available_mb)} available right now
|
|
298
|
+
f"machine only has {_fmt_mb(available_mb)} available right now, not enough for "
|
|
299
|
+
f"this app ({scale}, about {_fmt_mb(need_mb)} of heap).\n\n"
|
|
238
300
|
f"Running this would push the machine into swap and most likely get JADX killed by "
|
|
239
301
|
f"the OS out-of-memory killer (and may take other apps down with it). Options:\n"
|
|
240
302
|
f" - free up memory and try again\n"
|
|
@@ -30,8 +30,14 @@ JADX_BIN = os.path.join(JADX_HOME, "bin", "jadx.bat" if _IS_WINDOWS else "jadx")
|
|
|
30
30
|
# jadx 1.5.0 needs Java 11+; a JRE is fetched when the system has none.
|
|
31
31
|
_JRE_MAJOR = "17"
|
|
32
32
|
_JRE_ADOPTIUM_OS = "windows" if _IS_WINDOWS else ("mac" if _IS_MAC else "linux")
|
|
33
|
-
#
|
|
34
|
-
|
|
33
|
+
# Apple Silicon and ARM Linux get the native aarch64 build (an x64 JRE needs
|
|
34
|
+
# Rosetta on a Mac and does not run at all on ARM Linux). Windows stays x64:
|
|
35
|
+
# Temurin 17 has no Windows ARM JRE, and Windows on ARM emulates x64.
|
|
36
|
+
_JRE_ADOPTIUM_ARCH = (
|
|
37
|
+
"aarch64"
|
|
38
|
+
if not _IS_WINDOWS and platform.machine().lower() in ("arm64", "aarch64")
|
|
39
|
+
else "x64"
|
|
40
|
+
)
|
|
35
41
|
JRE_URL = (
|
|
36
42
|
f"https://api.adoptium.net/v3/binary/latest/{_JRE_MAJOR}/ga/"
|
|
37
43
|
f"{_JRE_ADOPTIUM_OS}/{_JRE_ADOPTIUM_ARCH}/jre/hotspot/normal/eclipse"
|
|
@@ -69,7 +75,7 @@ def resolve_jadx_binary():
|
|
|
69
75
|
|
|
70
76
|
def _java_major_version(java_bin):
|
|
71
77
|
try:
|
|
72
|
-
result = subprocess.run([java_bin, "-version"], capture_output=True, text=True, timeout=10)
|
|
78
|
+
result = subprocess.run([java_bin, "-version"], capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=10)
|
|
73
79
|
except Exception:
|
|
74
80
|
return None
|
|
75
81
|
out = result.stderr or result.stdout or ""
|
|
@@ -87,10 +93,12 @@ def _jre_bin_dir():
|
|
|
87
93
|
# Discovered, not hardcoded: the Adoptium top-level dir carries the patch version.
|
|
88
94
|
if not os.path.isdir(JRE_HOME):
|
|
89
95
|
return None
|
|
90
|
-
for entry in os.listdir(JRE_HOME):
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
96
|
+
for entry in sorted(os.listdir(JRE_HOME)):
|
|
97
|
+
# Linux/Windows: <jdk-17...-jre>/bin. macOS: <jdk-17...-jre>/Contents/Home/bin.
|
|
98
|
+
for sub in (("bin",), ("Contents", "Home", "bin")):
|
|
99
|
+
candidate = os.path.join(JRE_HOME, entry, *sub)
|
|
100
|
+
if os.path.isdir(candidate):
|
|
101
|
+
return candidate
|
|
94
102
|
return None
|
|
95
103
|
|
|
96
104
|
|
|
@@ -133,16 +141,31 @@ def resolve_java_home():
|
|
|
133
141
|
|
|
134
142
|
bin_dir = _jre_bin_dir()
|
|
135
143
|
if bin_dir:
|
|
136
|
-
|
|
144
|
+
java = os.path.join(bin_dir, "java.exe" if _IS_WINDOWS else "java")
|
|
145
|
+
# A JRE left by an older narvy can be the wrong architecture (x64 on
|
|
146
|
+
# Apple Silicon without Rosetta): it must actually run, or it is replaced.
|
|
147
|
+
if (_java_major_version(java) or 0) >= 11:
|
|
148
|
+
return bin_dir
|
|
149
|
+
logger.warning(f"The cached JRE in {JRE_HOME} does not run here - downloading a fresh one")
|
|
137
150
|
|
|
138
151
|
return _download_jre()
|
|
139
152
|
|
|
140
153
|
|
|
141
154
|
def _jadx_cmd(jadx_bin, extra_args):
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
155
|
+
"""argv for jadx. On Windows a .bat goes through cmd.exe as ONE string.
|
|
156
|
+
|
|
157
|
+
A list like ["cmd", "/c", bat, ...] breaks as soon as the .bat path is quoted
|
|
158
|
+
(a space in the user name, or C:\\Program Files): cmd /c strips the first
|
|
159
|
+
and the last quote of the line and runs `C:\\Users\\Jean`. With /s and the
|
|
160
|
+
whole line wrapped in one extra pair of quotes, cmd strips exactly that pair.
|
|
161
|
+
"""
|
|
162
|
+
if _IS_WINDOWS and jadx_bin.lower().endswith((".bat", ".cmd")):
|
|
163
|
+
return 'cmd /d /s /c "' + subprocess.list2cmdline([jadx_bin] + list(extra_args)) + '"'
|
|
164
|
+
return [jadx_bin] + list(extra_args)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _cmd_text(cmd):
|
|
168
|
+
return cmd if isinstance(cmd, str) else " ".join(str(c) for c in cmd)
|
|
146
169
|
|
|
147
170
|
|
|
148
171
|
def _validate_apk_zip(apk_path: str):
|
|
@@ -199,6 +222,10 @@ def _as_limit_preexec(max_mem_mb):
|
|
|
199
222
|
return _set_limits
|
|
200
223
|
|
|
201
224
|
|
|
225
|
+
# Set by decompile_apk when it ran jadx with a smaller heap than --max-mem; the caller prints it.
|
|
226
|
+
LAST_HEAP_NOTE = None
|
|
227
|
+
|
|
228
|
+
|
|
202
229
|
def decompile_apk(apk_path: str, output_dir: str, max_mem: str = "4g", force: bool = False,
|
|
203
230
|
extra_inputs=None):
|
|
204
231
|
"""Decompile an APK with jadx; returns (success, error_detail). jadx keeps the FIRST input's manifest, so apk_path stays first; `extra_inputs` merge into one tree."""
|
|
@@ -206,10 +233,16 @@ def decompile_apk(apk_path: str, output_dir: str, max_mem: str = "4g", force: bo
|
|
|
206
233
|
if validation_error:
|
|
207
234
|
return False, validation_error
|
|
208
235
|
|
|
236
|
+
global LAST_HEAP_NOTE
|
|
237
|
+
LAST_HEAP_NOTE = None
|
|
209
238
|
if not force:
|
|
210
239
|
verdict = check_memory_preflight(apk_path, max_mem)
|
|
211
240
|
if verdict.should_block:
|
|
212
241
|
return False, verdict.message
|
|
242
|
+
lowered = getattr(verdict, "lowered_max_mem", None)
|
|
243
|
+
if lowered:
|
|
244
|
+
max_mem = lowered
|
|
245
|
+
LAST_HEAP_NOTE = verdict.message
|
|
213
246
|
|
|
214
247
|
try:
|
|
215
248
|
jadx_bin = resolve_jadx_binary()
|
|
@@ -260,7 +293,7 @@ def decompile_apk(apk_path: str, output_dir: str, max_mem: str = "4g", force: bo
|
|
|
260
293
|
process = subprocess.run(
|
|
261
294
|
cmd,
|
|
262
295
|
capture_output=True,
|
|
263
|
-
text=True,
|
|
296
|
+
text=True, encoding="utf-8", errors="replace",
|
|
264
297
|
check=True,
|
|
265
298
|
timeout=900,
|
|
266
299
|
env=jadx_env,
|
|
@@ -269,7 +302,7 @@ def decompile_apk(apk_path: str, output_dir: str, max_mem: str = "4g", force: bo
|
|
|
269
302
|
return True, ""
|
|
270
303
|
except subprocess.CalledProcessError as e:
|
|
271
304
|
# jadx/cmd.exe don't consistently use stderr; report both streams.
|
|
272
|
-
parts = [f"JADX failed (exit code {e.returncode})", f"Command run: {
|
|
305
|
+
parts = [f"JADX failed (exit code {e.returncode})", f"Command run: {_cmd_text(cmd)}"]
|
|
273
306
|
if e.stdout:
|
|
274
307
|
parts.append(f"JADX stdout:\n{e.stdout}")
|
|
275
308
|
if e.stderr:
|
|
@@ -279,7 +312,7 @@ def decompile_apk(apk_path: str, output_dir: str, max_mem: str = "4g", force: bo
|
|
|
279
312
|
"No output captured on either stdout or stderr. Run "
|
|
280
313
|
"`narvy doctor` to check your Java/jadx setup, or try "
|
|
281
314
|
f"running this exact command yourself to see the raw error: "
|
|
282
|
-
f"{
|
|
315
|
+
f"{_cmd_text(cmd)}"
|
|
283
316
|
)
|
|
284
317
|
# OOM is unambiguous; a bare -9 (SIGKILL) is ambiguous, so they differ.
|
|
285
318
|
_combined_output = (e.stdout or "") + (e.stderr or "")
|
|
@@ -18,7 +18,7 @@ console = Console()
|
|
|
18
18
|
|
|
19
19
|
def _java_version(java_bin):
|
|
20
20
|
"""Run `java -version` and return (version_line, major_version_int_or_None)."""
|
|
21
|
-
result = subprocess.run([java_bin, "-version"], capture_output=True, text=True, timeout=10)
|
|
21
|
+
result = subprocess.run([java_bin, "-version"], capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=10)
|
|
22
22
|
out = result.stderr or result.stdout or ""
|
|
23
23
|
version_line = out.splitlines()[0] if out else "unknown version"
|
|
24
24
|
# Handles both the old "1.8.0_501" scheme and the modern "17.0.2" one.
|
|
@@ -225,7 +225,7 @@ def scp_to_remote(conn: HostConn, local_path: str, remote_path: str,
|
|
|
225
225
|
argv = ["sshpass", "-e", *argv]
|
|
226
226
|
|
|
227
227
|
try:
|
|
228
|
-
proc = subprocess.run(argv, capture_output=True, text=True,
|
|
228
|
+
proc = subprocess.run(argv, capture_output=True, text=True, encoding="utf-8", errors="replace",
|
|
229
229
|
timeout=timeout, env=env)
|
|
230
230
|
except subprocess.TimeoutExpired as exc:
|
|
231
231
|
raise LynisBootstrapError(
|
|
@@ -85,7 +85,7 @@ def capture_host_key(hostname: str, port: int = 22,
|
|
|
85
85
|
try:
|
|
86
86
|
proc = subprocess.run(
|
|
87
87
|
["ssh-keyscan", "-p", str(port), "-T", str(timeout), hostname],
|
|
88
|
-
capture_output=True, text=True, timeout=timeout + 10,
|
|
88
|
+
capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=timeout + 10,
|
|
89
89
|
)
|
|
90
90
|
except (subprocess.TimeoutExpired, OSError):
|
|
91
91
|
return None
|
|
@@ -178,7 +178,7 @@ def ssh_exec(conn: HostConn, remote_cmd: str,
|
|
|
178
178
|
|
|
179
179
|
try:
|
|
180
180
|
proc = subprocess.run(
|
|
181
|
-
argv, capture_output=True, text=True, timeout=timeout, env=env,
|
|
181
|
+
argv, capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=timeout, env=env,
|
|
182
182
|
)
|
|
183
183
|
except subprocess.TimeoutExpired as exc:
|
|
184
184
|
raise SSHExecError(
|
|
@@ -54,7 +54,7 @@ def _icdump_class_count(binary_path: str, timeout: int = 60
|
|
|
54
54
|
try:
|
|
55
55
|
proc = subprocess.run(
|
|
56
56
|
[_sys.executable, "-c", _ICDUMP_PROBE_SCRIPT, binary_path],
|
|
57
|
-
capture_output=True, text=True, timeout=timeout,
|
|
57
|
+
capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=timeout,
|
|
58
58
|
)
|
|
59
59
|
except subprocess.TimeoutExpired:
|
|
60
60
|
return False, 0, 0, f"timed out after {timeout}s"
|
|
@@ -997,7 +997,7 @@ def analyze_ipa(ipa_path: str, override_config: Optional[ScopeConfig] = None) ->
|
|
|
997
997
|
framework_members = [n for n in names if n.startswith(frameworks_prefix)]
|
|
998
998
|
|
|
999
999
|
import tempfile
|
|
1000
|
-
with tempfile.TemporaryDirectory(prefix="narvy-ios-") as tmpdir:
|
|
1000
|
+
with tempfile.TemporaryDirectory(prefix="narvy-ios-", ignore_cleanup_errors=True) as tmpdir:
|
|
1001
1001
|
main_binary_path = _safe_extract_member(zf, main_binary_member, tmpdir)
|
|
1002
1002
|
if main_binary_path is None:
|
|
1003
1003
|
result["error"] = f"Failed to extract main binary {main_binary_member} " \
|