narvy-cli 1.1.5__tar.gz → 1.2.0__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.5 → narvy_cli-1.2.0}/CHANGELOG.md +36 -0
- {narvy_cli-1.1.5/narvy_cli.egg-info → narvy_cli-1.2.0}/PKG-INFO +46 -3
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/README.md +45 -2
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/narvy/__init__.py +1 -1
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/narvy/main.py +39 -1
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/narvy/reporter.py +6 -1
- narvy_cli-1.2.0/narvy/secret_output.py +232 -0
- narvy_cli-1.2.0/narvy/secret_presentation.py +1053 -0
- narvy_cli-1.2.0/narvy/store_check/__init__.py +2 -0
- narvy_cli-1.2.0/narvy/store_check/__main__.py +5 -0
- narvy_cli-1.2.0/narvy/store_check/analyze.py +73 -0
- narvy_cli-1.2.0/narvy/store_check/android/arsc.py +109 -0
- narvy_cli-1.2.0/narvy/store_check/android/axml.py +178 -0
- narvy_cli-1.2.0/narvy/store_check/android/checks.py +668 -0
- narvy_cli-1.2.0/narvy/store_check/android/elf.py +79 -0
- narvy_cli-1.2.0/narvy/store_check/android/loader.py +160 -0
- narvy_cli-1.2.0/narvy/store_check/android/protoxml.py +181 -0
- narvy_cli-1.2.0/narvy/store_check/android/xmlmodel.py +106 -0
- narvy_cli-1.2.0/narvy/store_check/archive.py +108 -0
- narvy_cli-1.2.0/narvy/store_check/cli.py +129 -0
- narvy_cli-1.2.0/narvy/store_check/data/__init__.py +0 -0
- narvy_cli-1.2.0/narvy/store_check/data/apple.py +195 -0
- narvy_cli-1.2.0/narvy/store_check/data/play.py +162 -0
- narvy_cli-1.2.0/narvy/store_check/ios/__init__.py +0 -0
- narvy_cli-1.2.0/narvy/store_check/ios/checks.py +467 -0
- narvy_cli-1.2.0/narvy/store_check/ios/loader.py +154 -0
- narvy_cli-1.2.0/narvy/store_check/ios/macho.py +352 -0
- narvy_cli-1.2.0/narvy/store_check/model.py +119 -0
- narvy_cli-1.2.0/narvy/web/__init__.py +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0/narvy_cli.egg-info}/PKG-INFO +46 -3
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/narvy_cli.egg-info/SOURCES.txt +23 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/LICENSE +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/MANIFEST.in +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/SECURITY.md +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/narvy/__main__.py +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/narvy/android/__init__.py +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/narvy/android/source_analyzer.py +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/narvy/android/split_bundle.py +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/narvy/android_rule_context.py +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/narvy/apk_memory_preflight.py +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/narvy/auth.py +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/narvy/ci_templates/bitbucket.yml +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/narvy/ci_templates/github.yml +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/narvy/ci_templates/gitlab.yml +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/narvy/cloud/__init__.py +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/narvy/cloud/aws_scan.py +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/narvy/comment_filter.py +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/narvy/crypto_taint_lite.py +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/narvy/decompiler.py +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/narvy/doctor.py +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/narvy/host/__init__.py +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/narvy/host/audit.py +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/narvy/host/host_knowledge.py +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/narvy/host/lynis_bootstrap.py +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/narvy/host/lynis_parser.py +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/narvy/host/narvy_checks.py +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/narvy/host/report.py +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/narvy/host/ssh_exec.py +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/narvy/ios/__init__.py +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/narvy/ios/binary_analyzer.py +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/narvy/ios/plist_checks.py +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/narvy/ios/source_analyzer.py +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/narvy/ios/third_party_filter.py +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/narvy/ios/trust_all_context.py +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/narvy/native_hardening.py +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/narvy/own_reports.py +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/narvy/pathhint.py +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/narvy/pathnorm.py +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/narvy/proc.py +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/narvy/rule_engine.py +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/narvy/rules/android/config.yml +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/narvy/rules/android/crypto.yml +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/narvy/rules/android/secrets.yml +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/narvy/rules/android/storage.yml +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/narvy/rules/android/webview.yml +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/narvy/rules/android.yml +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/narvy/rules/ios/objc/crypto.yml +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/narvy/rules/ios/objc/network.yml +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/narvy/rules/ios/objc/secrets.yml +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/narvy/rules/ios/objc/storage.yml +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/narvy/rules/ios/objc/webview.yml +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/narvy/rules/ios_swift.yml +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/narvy/rules/web/go.yml +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/narvy/rules/web/java.yml +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/narvy/rules/web/javascript.yml +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/narvy/rules/web/kotlin.yml +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/narvy/rules/web/php.yml +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/narvy/rules/web/python.yml +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/narvy/rules/web/ruby.yml +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/narvy/rules/web/rust.yml +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/narvy/rules/web/secrets.yml +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/narvy/rules/web/secrets_supplement.yml +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/narvy/sca/__init__.py +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/narvy/sca/android_deps.py +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/narvy/sca/ios_deps.py +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/narvy/sca/osv_client.py +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/narvy/sca/web_deps.py +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/narvy/scope_config.py +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/narvy/secret_value_grade.py +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/narvy/semgrep_engine.py +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/narvy/stack_protector_evidence.py +0 -0
- {narvy_cli-1.1.5/narvy/web → narvy_cli-1.2.0/narvy/store_check/android}/__init__.py +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/narvy/telemetry.py +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/narvy/third_party_filter.py +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/narvy/uploader.py +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/narvy/weak_prng_context.py +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/narvy/web/nuclei_binary.py +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/narvy/web/scan_blocklist.py +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/narvy/web/scanner.py +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/narvy/web/source_analyzer.py +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/narvy/web/ssrf_guard.py +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/narvy_cli.egg-info/dependency_links.txt +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/narvy_cli.egg-info/entry_points.txt +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/narvy_cli.egg-info/requires.txt +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/narvy_cli.egg-info/top_level.txt +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/pyproject.toml +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/setup.cfg +0 -0
- {narvy_cli-1.1.5 → narvy_cli-1.2.0}/setup.py +0 -0
|
@@ -5,6 +5,42 @@ 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.2.0]
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `narvy store-check <file>`: store readiness checks for an APK, AAB,
|
|
13
|
+
split-APK set (`.apks`, `.xapk`, `.apkm`) or IPA against the Google Play
|
|
14
|
+
and App Store upload requirements: target API level, 16 KB page size
|
|
15
|
+
(ELF segment alignment and zip alignment of native libraries),
|
|
16
|
+
`android:exported` on components with intent filters, debuggable builds,
|
|
17
|
+
cleartext traffic, foreground service types, permissions that need a Play
|
|
18
|
+
Console declaration, iOS SDK and minimum OS version, required-reason APIs
|
|
19
|
+
against the privacy manifests, listed third-party SDKs without a privacy
|
|
20
|
+
manifest, App Transport Security and purpose strings. Every finding cites
|
|
21
|
+
the value read in the binary and the official page the rule comes from;
|
|
22
|
+
a question the binary cannot settle is reported `not determined`, never
|
|
23
|
+
guessed. Options: `--json` (both languages), `--lang fr`, `--as-of DATE`
|
|
24
|
+
(store requirements are dated), `--fail-on warning`, `--verbose`. Exit
|
|
25
|
+
codes: 0 no blocker, 1 blocker (or warning with `--fail-on warning`),
|
|
26
|
+
3 file not analysable. The check runs offline, reads the archive in
|
|
27
|
+
memory, writes nothing to disk and sends no telemetry. Python standard
|
|
28
|
+
library only, no new dependency. It is not a security scan.
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
|
|
32
|
+
- Secret findings are worded for what they are: "Potential secret: <kind>
|
|
33
|
+
(format match, not tested)" or "(pattern match, not tested)". Narvy never
|
|
34
|
+
tests a secret (no login, no API call, no revocation check). Findings
|
|
35
|
+
carry `secret_confidence` (`likely` or `potential`) and `secret_tested:
|
|
36
|
+
false` in JSON and SARIF. Public client keys shipped in app code (Firebase
|
|
37
|
+
configuration, Google API keys restricted by design) are reported as
|
|
38
|
+
Info.
|
|
39
|
+
- Secret values are masked in every output: console table, JSON, SARIF,
|
|
40
|
+
the upload payload, raw tool output (`--verbose`) and log records. The
|
|
41
|
+
wording and masking are the same as the Narvy dashboard, so dashboard
|
|
42
|
+
fingerprints of uploaded findings do not change.
|
|
43
|
+
|
|
8
44
|
## [1.1.5]
|
|
9
45
|
|
|
10
46
|
### Changed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: narvy-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.0
|
|
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
|
|
@@ -56,6 +56,11 @@ access or your own existing credentials:
|
|
|
56
56
|
- `cloud-scan` - AWS account posture check using your own ambient AWS
|
|
57
57
|
credentials
|
|
58
58
|
|
|
59
|
+
and one check that is not a security scan:
|
|
60
|
+
|
|
61
|
+
- `store-check` - Google Play and App Store upload requirements for an
|
|
62
|
+
APK, AAB, split-APK set or IPA, offline (see [Store check](#store-check))
|
|
63
|
+
|
|
59
64
|
Your code, binaries and findings are never uploaded unless you explicitly
|
|
60
65
|
pass `--upload`. The CLI does send anonymous usage statistics, described
|
|
61
66
|
exactly in [Telemetry](#telemetry), which you can turn off.
|
|
@@ -181,6 +186,10 @@ narvy scans
|
|
|
181
186
|
|
|
182
187
|
# Scan + upload every .apk/.aab/.apkm/.xapk/.ipa found in a folder, in one command
|
|
183
188
|
narvy upload-all ./my-apps-folder/
|
|
189
|
+
|
|
190
|
+
# Google Play / App Store upload requirements, offline (not a security scan)
|
|
191
|
+
narvy store-check app-release.aab
|
|
192
|
+
narvy store-check MyApp.ipa --lang fr --json
|
|
184
193
|
```
|
|
185
194
|
|
|
186
195
|
Run `narvy doctor` before your first scan to check your Java/jadx/iOS
|
|
@@ -237,6 +246,40 @@ exhaust the machine. `NARVY_SEMGREP_JOBS` and `NARVY_SEMGREP_MAX_MEMORY_MB`
|
|
|
237
246
|
listed in the scan notes. Outside a terminal (CI logs), a progress line is
|
|
238
247
|
printed every 30 seconds; `NARVY_PROGRESS_INTERVAL` changes that (0 = off).
|
|
239
248
|
|
|
249
|
+
## Store check
|
|
250
|
+
|
|
251
|
+
`narvy store-check` answers one question: will Google Play or the App Store
|
|
252
|
+
accept this build, and what will they ask about it? It reads the manifest,
|
|
253
|
+
resources, native libraries, Info.plist, Mach-O load commands, imported
|
|
254
|
+
symbols and privacy manifests of the file, and evaluates them against the
|
|
255
|
+
store requirements in force at a date (`--as-of`, default today).
|
|
256
|
+
|
|
257
|
+
```bash
|
|
258
|
+
narvy store-check app-release.aab
|
|
259
|
+
narvy store-check app.xapk --fail-on warning # CI gate on warnings too
|
|
260
|
+
narvy store-check MyApp.ipa --json > store.json # EN and FR messages in one report
|
|
261
|
+
narvy store-check MyApp.ipa --as-of 2027-02-01 # evaluate a future deadline
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
| Store | Checks |
|
|
265
|
+
|-------|--------|
|
|
266
|
+
| Google Play | `PLAY-TARGET-SDK`, `PLAY-16KB-ELF`, `PLAY-16KB-ZIPALIGN`, `PLAY-EXPORTED`, `PLAY-DEBUGGABLE`, `PLAY-CLEARTEXT`, `PLAY-FGS-TYPE`, `PLAY-FGS-PERMISSION`, `PLAY-FGS-DECLARATION`, `PLAY-PERMISSION-DECLARATION` |
|
|
267
|
+
| App Store | `APPSTORE-SDK-VERSION`, `APPSTORE-MIN-OS`, `APPSTORE-REQUIRED-REASON-API`, `APPSTORE-PRIVACY-MANIFEST`, `APPSTORE-THIRD-PARTY-SDK`, `APPSTORE-ATS`, `APPSTORE-PURPOSE-STRINGS` |
|
|
268
|
+
|
|
269
|
+
Each check reports `fail` (severity `blocker`, `warning` or `info`),
|
|
270
|
+
`pass`, `not_determined` (the binary does not settle the question) or
|
|
271
|
+
`not_applicable`, with the evidence read from the file and the official
|
|
272
|
+
page the rule comes from. Store values and their sources live in
|
|
273
|
+
`narvy/store_check/data/`.
|
|
274
|
+
|
|
275
|
+
It runs offline: no network call, no telemetry event, nothing written to
|
|
276
|
+
disk (the archive is read in memory). It does not check store listing
|
|
277
|
+
metadata, Data safety answers, whether a declared permission use is
|
|
278
|
+
acceptable under Play policy, or SDK signatures.
|
|
279
|
+
|
|
280
|
+
Exit codes: 0 no blocker (with `--fail-on warning`, no warning either),
|
|
281
|
+
1 at least one blocker (or warning), 3 the file cannot be analysed.
|
|
282
|
+
|
|
240
283
|
## Dependency check (SCA)
|
|
241
284
|
|
|
242
285
|
The dependency check sends package names and versions (never code, file paths or findings) to Narvy's EU-hosted vulnerability database. Set OSV_API_URL to use another OSV-compatible server.
|
|
@@ -262,8 +305,8 @@ in the scan's coverage note. Results are cached for 7 days in
|
|
|
262
305
|
|
|
263
306
|
## Telemetry
|
|
264
307
|
|
|
265
|
-
The CLI sends one small anonymous event per command
|
|
266
|
-
scan types are used and where they fail. It is sent in the background with
|
|
308
|
+
The CLI sends one small anonymous event per command (`store-check` sends
|
|
309
|
+
none), so we can see which scan types are used and where they fail. It is sent in the background with
|
|
267
310
|
a 1 second timeout, is never retried or stored for later, and never fails a
|
|
268
311
|
scan. At most it adds 1 second at exit. A notice is printed the first time it runs.
|
|
269
312
|
|
|
@@ -23,6 +23,11 @@ access or your own existing credentials:
|
|
|
23
23
|
- `cloud-scan` - AWS account posture check using your own ambient AWS
|
|
24
24
|
credentials
|
|
25
25
|
|
|
26
|
+
and one check that is not a security scan:
|
|
27
|
+
|
|
28
|
+
- `store-check` - Google Play and App Store upload requirements for an
|
|
29
|
+
APK, AAB, split-APK set or IPA, offline (see [Store check](#store-check))
|
|
30
|
+
|
|
26
31
|
Your code, binaries and findings are never uploaded unless you explicitly
|
|
27
32
|
pass `--upload`. The CLI does send anonymous usage statistics, described
|
|
28
33
|
exactly in [Telemetry](#telemetry), which you can turn off.
|
|
@@ -148,6 +153,10 @@ narvy scans
|
|
|
148
153
|
|
|
149
154
|
# Scan + upload every .apk/.aab/.apkm/.xapk/.ipa found in a folder, in one command
|
|
150
155
|
narvy upload-all ./my-apps-folder/
|
|
156
|
+
|
|
157
|
+
# Google Play / App Store upload requirements, offline (not a security scan)
|
|
158
|
+
narvy store-check app-release.aab
|
|
159
|
+
narvy store-check MyApp.ipa --lang fr --json
|
|
151
160
|
```
|
|
152
161
|
|
|
153
162
|
Run `narvy doctor` before your first scan to check your Java/jadx/iOS
|
|
@@ -204,6 +213,40 @@ exhaust the machine. `NARVY_SEMGREP_JOBS` and `NARVY_SEMGREP_MAX_MEMORY_MB`
|
|
|
204
213
|
listed in the scan notes. Outside a terminal (CI logs), a progress line is
|
|
205
214
|
printed every 30 seconds; `NARVY_PROGRESS_INTERVAL` changes that (0 = off).
|
|
206
215
|
|
|
216
|
+
## Store check
|
|
217
|
+
|
|
218
|
+
`narvy store-check` answers one question: will Google Play or the App Store
|
|
219
|
+
accept this build, and what will they ask about it? It reads the manifest,
|
|
220
|
+
resources, native libraries, Info.plist, Mach-O load commands, imported
|
|
221
|
+
symbols and privacy manifests of the file, and evaluates them against the
|
|
222
|
+
store requirements in force at a date (`--as-of`, default today).
|
|
223
|
+
|
|
224
|
+
```bash
|
|
225
|
+
narvy store-check app-release.aab
|
|
226
|
+
narvy store-check app.xapk --fail-on warning # CI gate on warnings too
|
|
227
|
+
narvy store-check MyApp.ipa --json > store.json # EN and FR messages in one report
|
|
228
|
+
narvy store-check MyApp.ipa --as-of 2027-02-01 # evaluate a future deadline
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
| Store | Checks |
|
|
232
|
+
|-------|--------|
|
|
233
|
+
| Google Play | `PLAY-TARGET-SDK`, `PLAY-16KB-ELF`, `PLAY-16KB-ZIPALIGN`, `PLAY-EXPORTED`, `PLAY-DEBUGGABLE`, `PLAY-CLEARTEXT`, `PLAY-FGS-TYPE`, `PLAY-FGS-PERMISSION`, `PLAY-FGS-DECLARATION`, `PLAY-PERMISSION-DECLARATION` |
|
|
234
|
+
| App Store | `APPSTORE-SDK-VERSION`, `APPSTORE-MIN-OS`, `APPSTORE-REQUIRED-REASON-API`, `APPSTORE-PRIVACY-MANIFEST`, `APPSTORE-THIRD-PARTY-SDK`, `APPSTORE-ATS`, `APPSTORE-PURPOSE-STRINGS` |
|
|
235
|
+
|
|
236
|
+
Each check reports `fail` (severity `blocker`, `warning` or `info`),
|
|
237
|
+
`pass`, `not_determined` (the binary does not settle the question) or
|
|
238
|
+
`not_applicable`, with the evidence read from the file and the official
|
|
239
|
+
page the rule comes from. Store values and their sources live in
|
|
240
|
+
`narvy/store_check/data/`.
|
|
241
|
+
|
|
242
|
+
It runs offline: no network call, no telemetry event, nothing written to
|
|
243
|
+
disk (the archive is read in memory). It does not check store listing
|
|
244
|
+
metadata, Data safety answers, whether a declared permission use is
|
|
245
|
+
acceptable under Play policy, or SDK signatures.
|
|
246
|
+
|
|
247
|
+
Exit codes: 0 no blocker (with `--fail-on warning`, no warning either),
|
|
248
|
+
1 at least one blocker (or warning), 3 the file cannot be analysed.
|
|
249
|
+
|
|
207
250
|
## Dependency check (SCA)
|
|
208
251
|
|
|
209
252
|
The dependency check sends package names and versions (never code, file paths or findings) to Narvy's EU-hosted vulnerability database. Set OSV_API_URL to use another OSV-compatible server.
|
|
@@ -229,8 +272,8 @@ in the scan's coverage note. Results are cached for 7 days in
|
|
|
229
272
|
|
|
230
273
|
## Telemetry
|
|
231
274
|
|
|
232
|
-
The CLI sends one small anonymous event per command
|
|
233
|
-
scan types are used and where they fail. It is sent in the background with
|
|
275
|
+
The CLI sends one small anonymous event per command (`store-check` sends
|
|
276
|
+
none), so we can see which scan types are used and where they fail. It is sent in the background with
|
|
234
277
|
a 1 second timeout, is never retried or stored for later, and never fails a
|
|
235
278
|
scan. At most it adds 1 second at exit. A notice is printed the first time it runs.
|
|
236
279
|
|
|
@@ -41,6 +41,7 @@ from . import decompiler as _decompiler
|
|
|
41
41
|
from .apk_memory_preflight import is_auto as is_auto_mem
|
|
42
42
|
from .rule_engine import load_rules_from_dir, run_rules_on_file
|
|
43
43
|
from . import auth, own_reports, uploader, telemetry
|
|
44
|
+
from . import secret_output
|
|
44
45
|
from .third_party_filter import get_own_package_roots, resolve_file_scope, check_android_override
|
|
45
46
|
from .scope_config import load_scope_config
|
|
46
47
|
from . import semgrep_engine
|
|
@@ -63,6 +64,7 @@ from .web.scan_blocklist import check_blocklist, ScanBlocked, REFUSAL_MESSAGE
|
|
|
63
64
|
from .web.ssrf_guard import validate_url as web_validate_url, SSRFBlocked
|
|
64
65
|
from .host.ssh_exec import HostConn, SSHExecError, _validate_conn
|
|
65
66
|
from .host.audit import audit_host
|
|
67
|
+
from .store_check import cli as store_check_cli
|
|
66
68
|
from .host.report import normalize_host_findings, severity_counts as host_severity_counts
|
|
67
69
|
# cloud/aws_scan.py and its boto3 dependency are imported lazily inside
|
|
68
70
|
# cmd_cloud_scan(): boto3 is an opt-in extra, not a dependency of the whole CLI.
|
|
@@ -158,7 +160,7 @@ def _print_raw_tool_output(label, text):
|
|
|
158
160
|
if not text:
|
|
159
161
|
return
|
|
160
162
|
console.print(f"\n[dim]--- raw {label} output ---[/dim]")
|
|
161
|
-
console.print(f"[dim]{rich_escape(text)}[/dim]")
|
|
163
|
+
console.print(f"[dim]{rich_escape(secret_output.sp.mask_text(text))}[/dim]")
|
|
162
164
|
console.print("[dim]--- end raw output ---[/dim]\n")
|
|
163
165
|
|
|
164
166
|
|
|
@@ -1632,6 +1634,7 @@ def _scan_to_json(all_findings, rules, scan_mode, target_path, encrypted, scan_m
|
|
|
1632
1634
|
"recommendation": details.get("recommendation"),
|
|
1633
1635
|
"cwe": details.get("cwe"),
|
|
1634
1636
|
"masvs": details.get("masvs"),
|
|
1637
|
+
**secret_output.json_secret_fields(f),
|
|
1635
1638
|
})
|
|
1636
1639
|
if f.get("original_severity"):
|
|
1637
1640
|
out_findings[-1]["original_severity"] = f["original_severity"]
|
|
@@ -1847,6 +1850,11 @@ def cmd_scan(args):
|
|
|
1847
1850
|
console.print(f"[dim]Ignored Narvy report file(s) from an earlier run: {rich_escape(names)}"
|
|
1848
1851
|
f"{f' and {more} more' if more > 0 else ''}.[/dim]")
|
|
1849
1852
|
|
|
1853
|
+
# Honest secret wording + masking, once, on the final list: after every
|
|
1854
|
+
# severity decision, before the table, JSON, SARIF and the upload payload.
|
|
1855
|
+
# Narvy never tests a secret; no output (local or uploaded) carries a raw value.
|
|
1856
|
+
secret_output.present_cli_findings(all_findings, target_path, scan_mode)
|
|
1857
|
+
|
|
1850
1858
|
# Say so when a hybrid repo (RN/Flutter/Capacitor) has code this mode
|
|
1851
1859
|
# didn't cover, instead of exiting 0 silently.
|
|
1852
1860
|
uncovered_notes = _uncovered_surface_notes(target_path, scan_mode,
|
|
@@ -2398,6 +2406,8 @@ def _web_to_sarif(findings):
|
|
|
2398
2406
|
f.get("url", ""),
|
|
2399
2407
|
f.get("severity"),
|
|
2400
2408
|
) for f in findings]
|
|
2409
|
+
for res, f in zip(results, findings):
|
|
2410
|
+
res["properties"].update(secret_output.sarif_secret_properties(f))
|
|
2401
2411
|
return {
|
|
2402
2412
|
"$schema": "https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0-rtm.5.json",
|
|
2403
2413
|
"version": "2.1.0",
|
|
@@ -2464,6 +2474,8 @@ def cmd_web_scan(args):
|
|
|
2464
2474
|
_print_raw_tool_output("nuclei", result.get("stderr"))
|
|
2465
2475
|
|
|
2466
2476
|
findings = result.get("findings", [])
|
|
2477
|
+
# Honest secret wording + masking before any output (same list as result["findings"]).
|
|
2478
|
+
secret_output.present_web_findings(findings)
|
|
2467
2479
|
|
|
2468
2480
|
if args.format == "json":
|
|
2469
2481
|
output = json.dumps({"tool": "Narvy Web Scanner CLI", **result}, indent=2)
|
|
@@ -2718,6 +2730,23 @@ def _cloud_to_sarif(findings):
|
|
|
2718
2730
|
}
|
|
2719
2731
|
|
|
2720
2732
|
|
|
2733
|
+
STORE_CHECK_EXIT_CODES_HELP = (
|
|
2734
|
+
"exit codes:\n"
|
|
2735
|
+
" 0 no blocker (with --fail-on warning: no warning either)\n"
|
|
2736
|
+
" 1 at least one blocker (with --fail-on warning: or at least one warning)\n"
|
|
2737
|
+
" 3 the file cannot be analysed (not found, not an APK, AAB, split-APK set\n"
|
|
2738
|
+
" or IPA, damaged archive) or an option is invalid\n"
|
|
2739
|
+
" 130 interrupted (Ctrl-C, SIGTERM)\n\n"
|
|
2740
|
+
"Offline: no network call, no telemetry, nothing written to disk. Store\n"
|
|
2741
|
+
"requirements are dated; --as-of evaluates them at another date."
|
|
2742
|
+
)
|
|
2743
|
+
|
|
2744
|
+
|
|
2745
|
+
def cmd_store_check(args, parser):
|
|
2746
|
+
"""Store readiness checks (narvy.store_check). Not a security scan, no telemetry."""
|
|
2747
|
+
raise SystemExit(store_check_cli.run(args, parser, input_error_code=EXIT_BAD_TARGET))
|
|
2748
|
+
|
|
2749
|
+
|
|
2721
2750
|
def cmd_cloud_scan(args):
|
|
2722
2751
|
telemetry.note(surface="cloud")
|
|
2723
2752
|
_reject_format_as_file(args.output_file)
|
|
@@ -2903,6 +2932,8 @@ def _safe_console_streams():
|
|
|
2903
2932
|
|
|
2904
2933
|
def cli(prog=None):
|
|
2905
2934
|
_safe_console_streams()
|
|
2935
|
+
# Mask provider-format tokens, PEM blocks, JWTs and URL passwords in any log record.
|
|
2936
|
+
secret_output.sp.install_log_masking()
|
|
2906
2937
|
parser =_ArgumentParser(prog=prog, allow_abbrev=False, description=(
|
|
2907
2938
|
"Narvy CLI - Static Analysis for Android APKs/AABs, iOS IPAs, "
|
|
2908
2939
|
"Android/iOS source projects, and web/backend source projects "
|
|
@@ -3050,6 +3081,13 @@ def cli(prog=None):
|
|
|
3050
3081
|
p_cloud.add_argument("--fail-on", choices=["critical", "high", "medium", "low", "any"], help=FAIL_ON_HELP)
|
|
3051
3082
|
p_cloud.set_defaults(func=cmd_cloud_scan)
|
|
3052
3083
|
|
|
3084
|
+
p_store = sub.add_parser(
|
|
3085
|
+
"store-check", allow_abbrev=False, epilog=STORE_CHECK_EXIT_CODES_HELP,
|
|
3086
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
3087
|
+
help="Check an APK, AAB, XAPK or IPA against Google Play and App Store upload requirements (offline)")
|
|
3088
|
+
store_check_cli.add_arguments(p_store)
|
|
3089
|
+
p_store.set_defaults(func=lambda a: cmd_store_check(a, p_store))
|
|
3090
|
+
|
|
3053
3091
|
args = parser.parse_args()
|
|
3054
3092
|
# SIGTERM/SIGHUP (CI job cancel, docker stop, dropped SSH) take the Ctrl-C path so
|
|
3055
3093
|
# child scanners (semgrep workers) are torn down instead of orphaned.
|
|
@@ -2,6 +2,7 @@ import json
|
|
|
2
2
|
from typing import List, Dict, Any
|
|
3
3
|
|
|
4
4
|
from narvy import __version__
|
|
5
|
+
from narvy.secret_output import sarif_secret_properties
|
|
5
6
|
|
|
6
7
|
# Canonical SARIF severity mapping, shared by every emitter (scan + web/host/cloud).
|
|
7
8
|
# level = SARIF's per-result rank; security-severity = GitHub/GitLab 0-10 code-scanning score.
|
|
@@ -57,7 +58,10 @@ def generate_sarif_report(findings: List[Dict[str, Any]], rules: List[Dict[str,
|
|
|
57
58
|
"ruleId": finding['rule_id'],
|
|
58
59
|
"level": _SARIF_LEVEL.get(_sev, "warning"),
|
|
59
60
|
"message": {
|
|
60
|
-
"
|
|
61
|
+
# A presented secret leads with its honest title ("Potential secret: ...
|
|
62
|
+
# (format match, not tested)"); every other finding keeps its description.
|
|
63
|
+
"text": (finding.get('title') if finding.get('secret_presentation')
|
|
64
|
+
and finding.get('title') else finding['details']['description'])
|
|
61
65
|
},
|
|
62
66
|
"locations": [{
|
|
63
67
|
"physicalLocation": {
|
|
@@ -78,6 +82,7 @@ def generate_sarif_report(findings: List[Dict[str, Any]], rules: List[Dict[str,
|
|
|
78
82
|
"prngContextEvidence": finding['details'].get('prng_context_evidence', '')}
|
|
79
83
|
if isinstance(finding.get('details'), dict)
|
|
80
84
|
and finding['details'].get('prng_context') else {}),
|
|
85
|
+
**sarif_secret_properties(finding),
|
|
81
86
|
}
|
|
82
87
|
})
|
|
83
88
|
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"""CLI wiring of narvy/secret_presentation.py (honest secret wording + masking).
|
|
2
|
+
|
|
3
|
+
narvy/secret_presentation.py is a byte-identical copy of the hosted engine's
|
|
4
|
+
module, so the CLI and the dashboard word and mask a secret the same way.
|
|
5
|
+
This file only adapts the CLI finding shape to it:
|
|
6
|
+
|
|
7
|
+
* CLI findings keep description/cwe/recommendation in a nested ``details``
|
|
8
|
+
dict. The hosted ingest (cli_ingest.normalize_finding) lifts them to the top
|
|
9
|
+
level before fingerprinting, so the same lift happens here first: the
|
|
10
|
+
classifier then sees what the server sees, and ``fingerprint_mask_basis`` is
|
|
11
|
+
taken from exactly the inputs the server's fingerprint used before masking
|
|
12
|
+
existed (no fixed/new churn on the dashboard).
|
|
13
|
+
* CLI findings carry no snippet. To recognise the provider format (and so
|
|
14
|
+
word "format match" vs "pattern match", and spot public client keys) the
|
|
15
|
+
matched line is read from disk into a transient ``line_content`` key, used
|
|
16
|
+
by the classifier and removed again. It never reaches any output.
|
|
17
|
+
* Terminal, JSON and SARIF read ``details``; the honest, masked wording is
|
|
18
|
+
copied back there. Severity stays upper-case (the CLI convention).
|
|
19
|
+
|
|
20
|
+
Runs once per scan, after every severity decision (secret_value_grade) and
|
|
21
|
+
before any output or upload.
|
|
22
|
+
"""
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import os
|
|
26
|
+
from typing import Any, Dict, Iterable, List, Optional
|
|
27
|
+
|
|
28
|
+
from . import secret_presentation as sp
|
|
29
|
+
|
|
30
|
+
__all__ = ["present_cli_finding", "present_cli_findings", "present_web_findings",
|
|
31
|
+
"json_secret_fields", "sarif_secret_properties", "sp"]
|
|
32
|
+
|
|
33
|
+
_PEM_BEGIN = "PRIVATE KEY-----"
|
|
34
|
+
_PEM_MAX_LINES = 120
|
|
35
|
+
|
|
36
|
+
# Fields of a presented finding the local JSON report carries.
|
|
37
|
+
JSON_SECRET_FIELDS = (
|
|
38
|
+
"title", "title_fr", "description_fr", "remediation", "remediation_fr",
|
|
39
|
+
"severity_rationale", "severity_rationale_fr", "secret_confidence", "secret_tested",
|
|
40
|
+
"secret_public_by_design", "secret_kind", "secret_match_basis", "original_severity",
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _lift(f: Dict[str, Any], key: str, value: Any, added: List[str]) -> None:
|
|
45
|
+
if value in (None, "", [], {}):
|
|
46
|
+
return
|
|
47
|
+
if f.get(key) in (None, "", [], {}):
|
|
48
|
+
f[key] = value
|
|
49
|
+
added.append(key)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _resolve(file_path: str, root: Optional[str]) -> Optional[str]:
|
|
53
|
+
if not file_path:
|
|
54
|
+
return None
|
|
55
|
+
if os.path.isabs(file_path):
|
|
56
|
+
return file_path if os.path.isfile(file_path) else None
|
|
57
|
+
if root:
|
|
58
|
+
base = root if os.path.isdir(root) else os.path.dirname(root)
|
|
59
|
+
p = os.path.join(base, file_path)
|
|
60
|
+
if os.path.isfile(p):
|
|
61
|
+
return p
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _matched_text(f: Dict[str, Any], root: Optional[str],
|
|
66
|
+
cache: Dict[str, Optional[List[str]]]) -> Optional[str]:
|
|
67
|
+
"""The finding's source line (a whole PEM block when the line opens one)."""
|
|
68
|
+
try:
|
|
69
|
+
line_no = int(f.get("line") or 0)
|
|
70
|
+
except (TypeError, ValueError):
|
|
71
|
+
return None
|
|
72
|
+
path = _resolve(str(f.get("file_path") or ""), root)
|
|
73
|
+
if not path or line_no <= 0:
|
|
74
|
+
return None
|
|
75
|
+
if path not in cache:
|
|
76
|
+
try:
|
|
77
|
+
with open(path, "r", encoding="utf-8", errors="ignore") as fh:
|
|
78
|
+
cache[path] = fh.read().splitlines()
|
|
79
|
+
except OSError:
|
|
80
|
+
cache[path] = None
|
|
81
|
+
lines = cache[path]
|
|
82
|
+
if not lines or line_no > len(lines):
|
|
83
|
+
return None
|
|
84
|
+
text = lines[line_no - 1]
|
|
85
|
+
if _PEM_BEGIN in text and "BEGIN" in text:
|
|
86
|
+
block = [text]
|
|
87
|
+
for nxt in lines[line_no:line_no + _PEM_MAX_LINES]:
|
|
88
|
+
block.append(nxt)
|
|
89
|
+
if "END" in nxt and _PEM_BEGIN in nxt:
|
|
90
|
+
break
|
|
91
|
+
text = "\n".join(block)
|
|
92
|
+
return text
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# CLI scan mode -> the {platform, scan_mode} the hosted engine files the upload
|
|
96
|
+
# under (cli_ingest: binary vs source), so the wording ("app package" vs
|
|
97
|
+
# "source code") and the client-context call match the dashboard's.
|
|
98
|
+
_SCAN_CONTEXT = {
|
|
99
|
+
"android": {"platform": "android", "scan_mode": "binary"},
|
|
100
|
+
"android-bundle": {"platform": "android", "scan_mode": "binary"},
|
|
101
|
+
"ios-binary": {"platform": "ios", "scan_mode": "binary"},
|
|
102
|
+
"android-source": {"platform": "android", "scan_mode": "source"},
|
|
103
|
+
"ios-source": {"platform": "ios", "scan_mode": "source"},
|
|
104
|
+
"web-source": {"platform": "web", "scan_mode": "source"},
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def scan_context(scan_mode: Optional[str]) -> Optional[Dict[str, str]]:
|
|
109
|
+
return _SCAN_CONTEXT.get(str(scan_mode or ""))
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _present(f: Dict[str, Any], context: Optional[Dict[str, Any]]) -> bool:
|
|
113
|
+
return sp.present_finding(f, context) if context else sp.present_finding(f)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def present_cli_finding(f: Dict[str, Any], root: Optional[str] = None,
|
|
117
|
+
cache: Optional[Dict[str, Optional[List[str]]]] = None,
|
|
118
|
+
context: Optional[Dict[str, Any]] = None) -> bool:
|
|
119
|
+
"""Reword + mask one CLI finding in place. Returns True when it changed."""
|
|
120
|
+
if not isinstance(f, dict):
|
|
121
|
+
return False
|
|
122
|
+
cache = {} if cache is None else cache
|
|
123
|
+
details = f.get("details")
|
|
124
|
+
if isinstance(details, dict):
|
|
125
|
+
# Rule definitions share one details dict across all their findings
|
|
126
|
+
# (and with the SARIF rule descriptors): never write into it.
|
|
127
|
+
details = dict(details)
|
|
128
|
+
f["details"] = details
|
|
129
|
+
added: List[str] = []
|
|
130
|
+
if isinstance(details, dict):
|
|
131
|
+
_lift(f, "description", details.get("description"), added)
|
|
132
|
+
_lift(f, "cwe", details.get("cwe"), added)
|
|
133
|
+
elif isinstance(details, str):
|
|
134
|
+
_lift(f, "description", details, added)
|
|
135
|
+
|
|
136
|
+
transient = False
|
|
137
|
+
view = dict(f, **{k: v for k, v in (context or {}).items() if not f.get(k)})
|
|
138
|
+
if sp.is_secret_finding(view) and not f.get("secret_presentation"):
|
|
139
|
+
b = f.get("fingerprint_mask_basis")
|
|
140
|
+
if not (isinstance(b, dict) and b.get("scheme") == sp.MASK_BASIS_SCHEME):
|
|
141
|
+
# Taken before line_content exists: the server never saw that line.
|
|
142
|
+
f["fingerprint_mask_basis"] = sp.fingerprint_basis_of(f)
|
|
143
|
+
if "line_content" not in f:
|
|
144
|
+
text = _matched_text(f, root, cache)
|
|
145
|
+
if text:
|
|
146
|
+
f["line_content"] = text
|
|
147
|
+
transient = True
|
|
148
|
+
try:
|
|
149
|
+
changed = _present(f, context)
|
|
150
|
+
finally:
|
|
151
|
+
if transient:
|
|
152
|
+
f.pop("line_content", None)
|
|
153
|
+
|
|
154
|
+
if not changed and not f.get("secret_presentation"):
|
|
155
|
+
for k in added: # nothing to present: leave the finding as it was
|
|
156
|
+
f.pop(k, None)
|
|
157
|
+
return False
|
|
158
|
+
|
|
159
|
+
if isinstance(f.get("severity"), str):
|
|
160
|
+
f["severity"] = f["severity"].upper()
|
|
161
|
+
if isinstance(f.get("original_severity"), str):
|
|
162
|
+
f["original_severity"] = f["original_severity"].upper()
|
|
163
|
+
details = f.get("details") # present_finding may have replaced it (masked copy)
|
|
164
|
+
if isinstance(details, dict) and f.get("secret_presentation"):
|
|
165
|
+
details["description"] = f.get("description") or details.get("description")
|
|
166
|
+
if isinstance(f.get("remediation"), str):
|
|
167
|
+
details["recommendation"] = f["remediation"]
|
|
168
|
+
return True
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def present_cli_findings(findings: Iterable[Dict[str, Any]], root: Optional[str] = None,
|
|
172
|
+
scan_mode: Optional[str] = None) -> int:
|
|
173
|
+
"""present_cli_finding over a scan's final findings. Never raises."""
|
|
174
|
+
cache: Dict[str, Optional[List[str]]] = {}
|
|
175
|
+
context = scan_context(scan_mode)
|
|
176
|
+
n = 0
|
|
177
|
+
for f in findings or []:
|
|
178
|
+
try:
|
|
179
|
+
if present_cli_finding(f, root, cache, context):
|
|
180
|
+
n += 1
|
|
181
|
+
except Exception: # noqa: BLE001 - a presentation error must never lose a finding
|
|
182
|
+
try:
|
|
183
|
+
_mask_only(f)
|
|
184
|
+
except Exception: # noqa: BLE001
|
|
185
|
+
pass
|
|
186
|
+
return n
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _mask_only(f: Dict[str, Any]) -> None:
|
|
190
|
+
"""Fallback when wording failed: at least never show a format-matched value."""
|
|
191
|
+
for k, v in list(f.items()):
|
|
192
|
+
if isinstance(v, str) and k not in ("file_path", "rule_id", "severity"):
|
|
193
|
+
f[k] = sp.mask_text(v)
|
|
194
|
+
elif isinstance(v, dict):
|
|
195
|
+
f[k] = {kk: sp.mask_text(vv) if isinstance(vv, str) else vv for kk, vv in v.items()}
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def present_web_findings(findings: Iterable[Dict[str, Any]]) -> int:
|
|
199
|
+
"""web-scan findings: everything nuclei sees was served to browsers, so a
|
|
200
|
+
public-by-design key there is client context (same as the hosted web scanner)."""
|
|
201
|
+
n = 0
|
|
202
|
+
for f in findings or []:
|
|
203
|
+
if not isinstance(f, dict):
|
|
204
|
+
continue
|
|
205
|
+
try:
|
|
206
|
+
f.setdefault("secret_client_context", True)
|
|
207
|
+
if sp.present_finding(f):
|
|
208
|
+
n += 1
|
|
209
|
+
except Exception: # noqa: BLE001
|
|
210
|
+
try:
|
|
211
|
+
_mask_only(f)
|
|
212
|
+
except Exception: # noqa: BLE001
|
|
213
|
+
pass
|
|
214
|
+
return n
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def json_secret_fields(f: Dict[str, Any]) -> Dict[str, Any]:
|
|
218
|
+
"""Extra keys for the local JSON report (never secret_sha256 or basis internals)."""
|
|
219
|
+
if not f.get("secret_presentation"):
|
|
220
|
+
return {}
|
|
221
|
+
return {k: f[k] for k in JSON_SECRET_FIELDS if k in f}
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def sarif_secret_properties(f: Dict[str, Any]) -> Dict[str, Any]:
|
|
225
|
+
if not f.get("secret_presentation"):
|
|
226
|
+
return {}
|
|
227
|
+
out: Dict[str, Any] = {"secret_tested": False}
|
|
228
|
+
for k in ("secret_confidence", "secret_public_by_design", "secret_kind",
|
|
229
|
+
"secret_match_basis", "original_severity"):
|
|
230
|
+
if f.get(k) not in (None, ""):
|
|
231
|
+
out[k] = f[k]
|
|
232
|
+
return out
|