narvy-cli 1.0.1__tar.gz → 1.0.2__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.0.1/narvy_cli.egg-info → narvy_cli-1.0.2}/PKG-INFO +1 -1
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/__init__.py +1 -1
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/apk_memory_preflight.py +3 -3
- narvy_cli-1.0.2/narvy/ci_templates/bitbucket.yml +15 -0
- narvy_cli-1.0.2/narvy/ci_templates/github.yml +48 -0
- narvy_cli-1.0.2/narvy/ci_templates/gitlab.yml +19 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/doctor.py +6 -6
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/ios/binary_analyzer.py +10 -20
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/main.py +46 -24
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/rules/android.yml +115 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/rules/ios_swift.yml +34 -1
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/semgrep_engine.py +10 -10
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/web/scanner.py +7 -7
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/web/source_analyzer.py +1 -1
- {narvy_cli-1.0.1 → narvy_cli-1.0.2/narvy_cli.egg-info}/PKG-INFO +1 -1
- narvy_cli-1.0.1/narvy/ci_templates/bitbucket.yml +0 -25
- narvy_cli-1.0.1/narvy/ci_templates/github.yml +0 -60
- narvy_cli-1.0.1/narvy/ci_templates/gitlab.yml +0 -32
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/CHANGELOG.md +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/LICENSE +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/MANIFEST.in +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/README.md +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/SECURITY.md +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/android/__init__.py +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/android/source_analyzer.py +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/android/split_bundle.py +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/android_rule_context.py +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/auth.py +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/cloud/__init__.py +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/cloud/aws_scan.py +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/comment_filter.py +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/crypto_taint_lite.py +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/decompiler.py +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/host/__init__.py +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/host/audit.py +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/host/host_knowledge.py +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/host/lynis_bootstrap.py +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/host/lynis_parser.py +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/host/narvy_checks.py +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/host/report.py +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/host/ssh_exec.py +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/ios/__init__.py +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/ios/plist_checks.py +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/ios/source_analyzer.py +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/ios/third_party_filter.py +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/ios/trust_all_context.py +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/native_hardening.py +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/reporter.py +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/rule_engine.py +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/rules/android/config.yml +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/rules/android/crypto.yml +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/rules/android/secrets.yml +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/rules/android/storage.yml +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/rules/android/webview.yml +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/rules/ios/objc/crypto.yml +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/rules/ios/objc/network.yml +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/rules/ios/objc/secrets.yml +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/rules/ios/objc/storage.yml +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/rules/ios/objc/webview.yml +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/rules/web/go.yml +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/rules/web/java.yml +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/rules/web/javascript.yml +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/rules/web/kotlin.yml +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/rules/web/local/csharp_narvy/config.yml +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/rules/web/local/csharp_narvy/crypto.yml +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/rules/web/local/csharp_narvy/deserialization.yml +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/rules/web/local/csharp_narvy/injection.yml +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/rules/web/local/csharp_narvy/xxe.yml +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/rules/web/local/java_narvy/auth_jwt.yml +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/rules/web/local/java_narvy/deserialization.yml +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/rules/web/local/java_narvy/mybatis.yml +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/rules/web/local/java_narvy/snakeyaml.yml +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/rules/web/local/java_narvy/spring_authz.yml +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/rules/web/local/java_narvy/spring_config.yml +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/rules/web/local/java_narvy/spring_hardening.yml +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/rules/web/local/java_narvy/sqli.yml +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/rules/web/local/java_narvy/xxe.yml +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/rules/web/php.yml +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/rules/web/python.yml +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/rules/web/ruby.yml +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/rules/web/rust.yml +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/rules/web/secrets.yml +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/rules/web/secrets_supplement.yml +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/sca/__init__.py +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/sca/android_deps.py +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/sca/ios_deps.py +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/sca/osv_client.py +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/sca/web_deps.py +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/scope_config.py +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/stack_protector_evidence.py +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/third_party_filter.py +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/uploader.py +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/weak_prng_context.py +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/web/__init__.py +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/web/nuclei_binary.py +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/web/scan_blocklist.py +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/web/ssrf_guard.py +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy_cli.egg-info/SOURCES.txt +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy_cli.egg-info/dependency_links.txt +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy_cli.egg-info/entry_points.txt +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy_cli.egg-info/requires.txt +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy_cli.egg-info/top_level.txt +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/pyproject.toml +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/setup.cfg +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.2}/setup.py +0 -0
|
@@ -209,9 +209,9 @@ def check_memory_preflight(apk_path: str, max_mem: str,
|
|
|
209
209
|
f"\n\nRaising --max-mem on its own will not be enough: {suggest_gb}g of heap needs "
|
|
210
210
|
f"roughly {_fmt_mb(need_rss_mb)} of real memory, and this machine only has "
|
|
211
211
|
f"{_fmt_mb(available_mb)} available right now. Options:\n"
|
|
212
|
-
f" -
|
|
213
|
-
f" -
|
|
214
|
-
f" - run it anyway with --force if
|
|
212
|
+
f" - use the hosted scan (`--upload`): the platform decompiles any app size with no local memory ceiling\n"
|
|
213
|
+
f" - or free up that much memory (close other apps) and re-run with --max-mem {suggest_gb}g\n"
|
|
214
|
+
f" - or run it anyway with --force if your setup can take it"
|
|
215
215
|
)
|
|
216
216
|
else:
|
|
217
217
|
avail_note = (f" (this machine has {_fmt_mb(available_mb)} available)"
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Narvy SAST for Bitbucket Pipelines. Written by `narvy init-ci --bitbucket`; edit freely.
|
|
2
|
+
# Already have a bitbucket-pipelines.yml? Copy the step below into it instead of overwriting.
|
|
3
|
+
# Note: SARIF is stored as a build artifact (not Code Insights); the --fail-on gate still fails the build.
|
|
4
|
+
image: python:3.11
|
|
5
|
+
|
|
6
|
+
pipelines:
|
|
7
|
+
default:
|
|
8
|
+
- step:
|
|
9
|
+
name: Narvy SAST
|
|
10
|
+
script:
|
|
11
|
+
- pip install --upgrade pip
|
|
12
|
+
- pip install narvy-cli
|
|
13
|
+
- narvy scan . --output sarif --file narvy-results.sarif --fail-on high
|
|
14
|
+
artifacts:
|
|
15
|
+
- narvy-results.sarif
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Narvy SAST for GitHub Actions. Written by `narvy init-ci --github`; edit freely.
|
|
2
|
+
# Scans this repo, uploads SARIF to the Security tab, and fails the build on --fail-on.
|
|
3
|
+
name: Narvy SAST
|
|
4
|
+
|
|
5
|
+
on:
|
|
6
|
+
push:
|
|
7
|
+
branches: [ main, master ]
|
|
8
|
+
pull_request:
|
|
9
|
+
workflow_dispatch:
|
|
10
|
+
|
|
11
|
+
permissions:
|
|
12
|
+
contents: read
|
|
13
|
+
security-events: write # required to upload SARIF to code scanning
|
|
14
|
+
|
|
15
|
+
jobs:
|
|
16
|
+
narvy:
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v4
|
|
20
|
+
|
|
21
|
+
- uses: actions/setup-python@v5
|
|
22
|
+
with:
|
|
23
|
+
python-version: '3.11'
|
|
24
|
+
|
|
25
|
+
- name: Install Narvy CLI
|
|
26
|
+
run: |
|
|
27
|
+
python -m pip install --upgrade pip
|
|
28
|
+
pip install narvy-cli
|
|
29
|
+
|
|
30
|
+
- name: Run Narvy scan
|
|
31
|
+
# SARIF is written before the --fail-on gate, so the Security tab fills even on a failing build.
|
|
32
|
+
run: |
|
|
33
|
+
narvy scan . \
|
|
34
|
+
--output sarif \
|
|
35
|
+
--file narvy-results.sarif \
|
|
36
|
+
--fail-on high
|
|
37
|
+
|
|
38
|
+
- name: Upload SARIF to GitHub code scanning
|
|
39
|
+
if: always() # upload even when the scan step failed the build via --fail-on
|
|
40
|
+
uses: github/codeql-action/upload-sarif@v3
|
|
41
|
+
with:
|
|
42
|
+
sarif_file: narvy-results.sarif
|
|
43
|
+
|
|
44
|
+
# To also scan a web app URL, add a step:
|
|
45
|
+
# - run: narvy web-scan "https://staging.example.com" --format sarif --output-file web.sarif --fail-on high
|
|
46
|
+
# - if: always()
|
|
47
|
+
# uses: github/codeql-action/upload-sarif@v3
|
|
48
|
+
# with: { sarif_file: web.sarif, category: narvy-web }
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Narvy SAST for GitLab CI. Written by `narvy init-ci --gitlab`; edit freely.
|
|
2
|
+
# Already have a .gitlab-ci.yml? Copy the narvy_sast job below into it instead of overwriting.
|
|
3
|
+
# Note: SARIF is stored as a plain artifact (not GitLab's SAST schema), but the --fail-on gate still blocks the MR.
|
|
4
|
+
stages:
|
|
5
|
+
- test
|
|
6
|
+
|
|
7
|
+
narvy_sast:
|
|
8
|
+
stage: test
|
|
9
|
+
image: python:3.11-slim
|
|
10
|
+
before_script:
|
|
11
|
+
- pip install --upgrade pip
|
|
12
|
+
- pip install narvy-cli
|
|
13
|
+
script:
|
|
14
|
+
- narvy scan . --output sarif --file narvy-results.sarif --fail-on high
|
|
15
|
+
artifacts:
|
|
16
|
+
when: always # keep the SARIF even when --fail-on failed the pipeline
|
|
17
|
+
paths:
|
|
18
|
+
- narvy-results.sarif
|
|
19
|
+
expire_in: 1 week
|
|
@@ -87,7 +87,7 @@ def _check_icdump():
|
|
|
87
87
|
else:
|
|
88
88
|
detail = "Not installed - iOS binary scans will fall back to symbol-table-only Obj-C detection"
|
|
89
89
|
return True, detail, (
|
|
90
|
-
"Optional: `pip install narvy[ios-full]` (Linux/macOS/WSL only - no Windows wheel exists) "
|
|
90
|
+
"Optional: `pip install narvy-cli[ios-full]` (Linux/macOS/WSL only - no Windows wheel exists) "
|
|
91
91
|
"for full Obj-C class/method/property introspection on .ipa scans. Not required - iOS binary "
|
|
92
92
|
"scanning still works without it, just with coarser Obj-C detection (class names only, via LIEF's "
|
|
93
93
|
"symbol table, no methods/properties/protocols)."
|
|
@@ -110,10 +110,10 @@ def _check_memory():
|
|
|
110
110
|
)
|
|
111
111
|
if avail < 5 * 1024:
|
|
112
112
|
return True, detail, (
|
|
113
|
-
"Large commercial apps
|
|
114
|
-
"
|
|
115
|
-
"
|
|
116
|
-
"
|
|
113
|
+
"Large commercial apps need 8-24 GB of free memory to decompile locally. "
|
|
114
|
+
"The hosted scan (`--upload`) removes the local memory ceiling and handles "
|
|
115
|
+
"any app size - use it for large apps, or scan locally within this "
|
|
116
|
+
"machine's memory."
|
|
117
117
|
)
|
|
118
118
|
return True, detail, None
|
|
119
119
|
|
|
@@ -183,7 +183,7 @@ def _check_boto3():
|
|
|
183
183
|
except ImportError:
|
|
184
184
|
return True, "Not installed", (
|
|
185
185
|
"Needed only for `narvy cloud-scan`. Run: "
|
|
186
|
-
"pip install narvy[cloud]"
|
|
186
|
+
"pip install narvy-cli[cloud]"
|
|
187
187
|
)
|
|
188
188
|
|
|
189
189
|
|
|
@@ -1021,25 +1021,15 @@ def analyze_ipa(ipa_path: str, override_config: Optional[ScopeConfig] = None) ->
|
|
|
1021
1021
|
result["encrypted"] = encrypted
|
|
1022
1022
|
if encrypted:
|
|
1023
1023
|
result["encrypted_message"] = (
|
|
1024
|
-
"This IPA is FairPlay-encrypted (Apple
|
|
1025
|
-
"
|
|
1026
|
-
"
|
|
1027
|
-
"
|
|
1028
|
-
"
|
|
1029
|
-
"
|
|
1030
|
-
"
|
|
1031
|
-
"run
|
|
1032
|
-
"
|
|
1033
|
-
"(load commands are never encrypted), and Objective-C class/method "
|
|
1034
|
-
"metadata recovery (this metadata typically lives outside the "
|
|
1035
|
-
"encrypted __TEXT range). What is unreliable: weak-"
|
|
1036
|
-
"crypto/hardcoded-secret/dangerous-API string matching against the "
|
|
1037
|
-
"app's OWN literal string constants, since those live inside the "
|
|
1038
|
-
"encrypted region and read as ciphertext noise - expect a near-empty "
|
|
1039
|
-
"result for those specific checks, not a false 'clean bill of "
|
|
1040
|
-
"health'. The ONLY way to read that encrypted code is a decrypted "
|
|
1041
|
-
"binary (a jailbroken-device dump). `--upload` does NOT decrypt "
|
|
1042
|
-
"anything: the same encrypted binary is analysed, not decrypted."
|
|
1024
|
+
"This IPA is FairPlay-encrypted (Apple App Store DRM). Narvy covers "
|
|
1025
|
+
"it across two surfaces. The hosted engine runs its full rule set + "
|
|
1026
|
+
"ML triage + MASVS mapping over the readable, cleartext surface "
|
|
1027
|
+
"(Info.plist, entitlements, URL schemes / ATS, linked and embedded "
|
|
1028
|
+
"frameworks, Objective-C class/method metadata), and Narvy dynamic "
|
|
1029
|
+
"analysis executes the app decrypted on a real device to cover the "
|
|
1030
|
+
"app's own code and strings. This local run reads the cleartext "
|
|
1031
|
+
"surface only - run `narvy scan --upload` and Narvy dynamic analysis "
|
|
1032
|
+
"on the platform for the complete picture of this app."
|
|
1043
1033
|
)
|
|
1044
1034
|
|
|
1045
1035
|
findings = _analyze_one_binary(main_binary_path, exe_name, is_main_binary=True)
|
|
@@ -1087,7 +1077,7 @@ def analyze_ipa(ipa_path: str, override_config: Optional[ScopeConfig] = None) ->
|
|
|
1087
1077
|
result["notes"].append(
|
|
1088
1078
|
"icdump not installed - full Obj-C class/method/property "
|
|
1089
1079
|
"introspection needs Linux/macOS/WSL "
|
|
1090
|
-
"(pip install narvy[ios-full]). Falling back to binary "
|
|
1080
|
+
"(pip install narvy-cli[ios-full]). Falling back to binary "
|
|
1091
1081
|
"symbol-table scanning only."
|
|
1092
1082
|
)
|
|
1093
1083
|
result["icdump_used"] = icdump_used
|
|
@@ -18,6 +18,19 @@ from rich import box as rich_box
|
|
|
18
18
|
from rich.markup import escape as rich_escape
|
|
19
19
|
from rich.prompt import Confirm
|
|
20
20
|
|
|
21
|
+
import warnings
|
|
22
|
+
# Silence third-party import noise (requests RequestsDependencyWarning + urllib3
|
|
23
|
+
# 'strict' FutureWarning) only; runs before narvy imports pull in requests.
|
|
24
|
+
warnings.filterwarnings("ignore", category=FutureWarning, module=r"urllib3(\..*)?")
|
|
25
|
+
try:
|
|
26
|
+
with warnings.catch_warnings():
|
|
27
|
+
warnings.simplefilter("ignore")
|
|
28
|
+
from requests.exceptions import RequestsDependencyWarning as _RequestsDependencyWarning
|
|
29
|
+
warnings.filterwarnings("ignore", category=_RequestsDependencyWarning)
|
|
30
|
+
except Exception:
|
|
31
|
+
# If the class ever moves, fall back to a module-scoped filter on requests.
|
|
32
|
+
warnings.filterwarnings("ignore", module=r"requests(\..*)?")
|
|
33
|
+
|
|
21
34
|
from narvy import __version__
|
|
22
35
|
from narvy.reporter import generate_sarif_report, sarif_level, sarif_security_severity
|
|
23
36
|
from .decompiler import decompile_apk
|
|
@@ -374,7 +387,8 @@ def cmd_scans(args):
|
|
|
374
387
|
if not scans:
|
|
375
388
|
console.print(
|
|
376
389
|
"[bold yellow]No scans found on your account yet.[/bold yellow] "
|
|
377
|
-
"Run `narvy scan <path
|
|
390
|
+
"Run `narvy scan <path>` to scan locally. Submitting results here "
|
|
391
|
+
"with `--upload` needs a paid plan (Starter and up)."
|
|
378
392
|
)
|
|
379
393
|
return
|
|
380
394
|
|
|
@@ -1089,9 +1103,10 @@ def cmd_scan(args):
|
|
|
1089
1103
|
if not all_findings:
|
|
1090
1104
|
if encrypted_scan:
|
|
1091
1105
|
console.print(
|
|
1092
|
-
"[bold yellow]No issues
|
|
1093
|
-
"FairPlay-encrypted binary (see the encryption notice above)
|
|
1094
|
-
"
|
|
1106
|
+
"[bold yellow]No issues in the readable surface of this "
|
|
1107
|
+
"FairPlay-encrypted binary (see the encryption notice above). The app's "
|
|
1108
|
+
"own encrypted code is covered by Narvy dynamic analysis on the platform "
|
|
1109
|
+
"- run it there for the complete picture.[/bold yellow]"
|
|
1095
1110
|
)
|
|
1096
1111
|
else:
|
|
1097
1112
|
console.print("[bold green]No issues found based on the current rule set.[/bold green]")
|
|
@@ -1238,11 +1253,11 @@ def cmd_scan(args):
|
|
|
1238
1253
|
elif scan_mode == "android-source":
|
|
1239
1254
|
console.print(f"Scanned with {len(rules)} local rules against the raw source checkout. Narvy Cloud runs deeper polyglot SAST against your source, with MASVS mapping and ML triage.")
|
|
1240
1255
|
elif scan_mode == "web-source":
|
|
1241
|
-
console.print(f"Scanned with {len(rules)} local rules against the raw source checkout. Narvy Cloud runs
|
|
1256
|
+
console.print(f"Scanned with {len(rules)} local rules against the raw source checkout. Narvy Cloud runs the full polyglot web SAST engine (more languages and rule packs, SDLC/compliance mapping, ML triage).")
|
|
1242
1257
|
elif scan_mode == "ios-binary":
|
|
1243
1258
|
console.print(f"Scanned with {len(rules)} local rules against the compiled binary only. Narvy Cloud runs full iOS SAST (deeper Mach-O/ObjC/Swift analysis, MASVS mapping, ML triage) plus DAST.")
|
|
1244
1259
|
else:
|
|
1245
|
-
console.print(f"Scanned with {len(rules)} local rules. Narvy Cloud runs
|
|
1260
|
+
console.print(f"Scanned with {len(rules)} local rules. Narvy Cloud runs the full iOS source SAST engine (deeper Swift/ObjC semantic analysis, MASVS mapping, ML triage).")
|
|
1246
1261
|
console.print("[dim]This is a flat, unfiltered list (one row per rule hit). The cloud groups, de-duplicates and false-positive-filters the same findings and shows a severity-ranked default view, so its headline count for the same code is often different - and can be smaller - by design, not because it found less.[/dim]")
|
|
1247
1262
|
console.print("Run `narvy login` then re-run with [bold]--upload[/bold] for the full server-side scan (compliance mapping, ML triage, dashboard history) - [link=https://narvy.io]narvy.io[/link]")
|
|
1248
1263
|
|
|
@@ -1270,9 +1285,9 @@ def cmd_scan(args):
|
|
|
1270
1285
|
)
|
|
1271
1286
|
if encrypted_scan:
|
|
1272
1287
|
coverage_gaps.append(
|
|
1273
|
-
"This IPA is FairPlay-encrypted
|
|
1274
|
-
"
|
|
1275
|
-
"
|
|
1288
|
+
"This IPA is FairPlay-encrypted: this local run covered the cleartext "
|
|
1289
|
+
"surface. The app's own encrypted code is covered by Narvy dynamic analysis "
|
|
1290
|
+
"on the platform - run it there for complete coverage."
|
|
1276
1291
|
)
|
|
1277
1292
|
if scan_mode in ("android", "android-bundle", "android-source",
|
|
1278
1293
|
"ios-source", "web-source"):
|
|
@@ -1818,7 +1833,7 @@ def cmd_cloud_scan(args):
|
|
|
1818
1833
|
console.print(
|
|
1819
1834
|
"[bold red]ERROR:[/bold red] the AWS cloud scanner needs boto3, which isn't "
|
|
1820
1835
|
"installed (it's an opt-in extra to keep the base install light).\n"
|
|
1821
|
-
"Run: [bold]pip install narvy\\[cloud][/bold] (or `pip install boto3` "
|
|
1836
|
+
"Run: [bold]pip install narvy-cli\\[cloud][/bold] (or `pip install boto3` "
|
|
1822
1837
|
"into the same environment)."
|
|
1823
1838
|
)
|
|
1824
1839
|
raise SystemExit(2)
|
|
@@ -1954,7 +1969,7 @@ def cmd_init_ci(args):
|
|
|
1954
1969
|
|
|
1955
1970
|
console.print(f"[bold green]Wrote {platform} CI template to {dest}[/bold green]")
|
|
1956
1971
|
console.print(
|
|
1957
|
-
"[dim]It installs the CLI from PyPI with [bold]pip install narvy[/bold] "
|
|
1972
|
+
"[dim]It installs the CLI from PyPI with [bold]pip install narvy-cli[/bold] "
|
|
1958
1973
|
"and runs on every push and pull request.[/dim]"
|
|
1959
1974
|
)
|
|
1960
1975
|
console.print(
|
|
@@ -1965,7 +1980,7 @@ def cmd_init_ci(args):
|
|
|
1965
1980
|
|
|
1966
1981
|
|
|
1967
1982
|
def cli():
|
|
1968
|
-
parser = argparse.ArgumentParser(description=(
|
|
1983
|
+
parser = argparse.ArgumentParser(allow_abbrev=False, description=(
|
|
1969
1984
|
"Narvy CLI - Static Analysis for Android APKs/AABs, iOS IPAs, "
|
|
1970
1985
|
"Android/iOS source projects, and web/backend source projects "
|
|
1971
1986
|
f"({web_source_analyzer.SUPPORTED_LANGUAGES_LABEL})"
|
|
@@ -1977,24 +1992,24 @@ def cli():
|
|
|
1977
1992
|
)
|
|
1978
1993
|
sub = parser.add_subparsers(dest="command", required=True)
|
|
1979
1994
|
|
|
1980
|
-
p_login = sub.add_parser("login", help="Link this machine to your Narvy account")
|
|
1995
|
+
p_login = sub.add_parser("login", allow_abbrev=False, help="Link this machine to your Narvy account")
|
|
1981
1996
|
p_login.add_argument("--token", help="API token (skips interactive prompt)")
|
|
1982
1997
|
p_login.set_defaults(func=cmd_login)
|
|
1983
1998
|
|
|
1984
|
-
p_logout = sub.add_parser("logout", help="Remove the locally stored API token")
|
|
1999
|
+
p_logout = sub.add_parser("logout", allow_abbrev=False, help="Remove the locally stored API token")
|
|
1985
2000
|
p_logout.set_defaults(func=cmd_logout)
|
|
1986
2001
|
|
|
1987
|
-
p_scans = sub.add_parser("scans", help="List recent scans on your Narvy account (requires `narvy login`)")
|
|
2002
|
+
p_scans = sub.add_parser("scans", allow_abbrev=False, help="List recent scans on your Narvy account (requires `narvy login`)")
|
|
1988
2003
|
p_scans.add_argument("--limit", type=_positive_int, default=20, help="Max number of scans to show (default: 20, must be >= 1)")
|
|
1989
2004
|
p_scans.add_argument("--status", choices=["queued", "running", "done", "failed", "pending_sast", "scheduled"],
|
|
1990
2005
|
help="Filter by scan status")
|
|
1991
2006
|
p_scans.add_argument("--format", choices=["text", "json"], default="text", help="Output format (default: text)")
|
|
1992
2007
|
p_scans.set_defaults(func=cmd_scans)
|
|
1993
2008
|
|
|
1994
|
-
p_doctor = sub.add_parser("doctor", help="Check your Java/jadx/network/iOS-tooling setup before scanning")
|
|
2009
|
+
p_doctor = sub.add_parser("doctor", allow_abbrev=False, help="Check your Java/jadx/network/iOS-tooling setup before scanning")
|
|
1995
2010
|
p_doctor.set_defaults(func=lambda args: run_doctor())
|
|
1996
2011
|
|
|
1997
|
-
p_scan = sub.add_parser("scan", help="Scan an APK/AAB, an IPA, an Android/iOS source directory, or a web/backend source directory (local rules; add --upload for a full server-side scan, mobile-only)")
|
|
2012
|
+
p_scan = sub.add_parser("scan", allow_abbrev=False, help="Scan an APK/AAB, an IPA, an Android/iOS source directory, or a web/backend source directory (local rules; add --upload for a full server-side scan, mobile-only)")
|
|
1998
2013
|
p_scan.add_argument("apk_path", help=(f"Path to scan: an .apk or .aab (Android binary), an .apkm / .xapk / .apks split-APK bundle (APKMirror / APKPure / bundletool - the app module is scanned, plus the native libraries from its architecture split), an .ipa (iOS binary - unencrypted only, see docs), a directory containing an Android Gradle project (Android source), a directory containing an Xcode/Swift project (iOS source), or a directory containing a web/backend project "
|
|
1999
2014
|
f"({web_source_analyzer.SUPPORTED_LANGUAGES_LABEL}) - web source."))
|
|
2000
2015
|
p_scan.add_argument("--max-mem", default="4g", help="Maximum memory for JADX (e.g., '2g', '4096m'). Android-only, ignored for iOS scans. Defaults to 4g.")
|
|
@@ -2003,8 +2018,8 @@ def cli():
|
|
|
2003
2018
|
p_scan.add_argument("--output", "-o", default="console", choices=["console", "sarif", "json"],
|
|
2004
2019
|
type=str.lower, help="Output format: console (default), sarif or json. sarif and json are written to a file (see --file) so a CI step can consume them - the SARIF suits GitHub/GitLab code scanning, the JSON suits a plain jq step.")
|
|
2005
2020
|
p_scan.add_argument("--file", "-f", default=None, help="Output file for --output sarif/json (default: narvy-results.sarif or narvy-results.json). Ignored for console output.")
|
|
2006
|
-
p_scan.add_argument("--upload", action="store_true", help="Submit this scan's RESULTS to your Narvy account for server-side curation (ML false-positive filter, dedup, dashboard history) - the dashboard then shows exactly what this scan found, curated. Works for binaries AND source directories. Requires `narvy login`
|
|
2007
|
-
p_scan.add_argument("--upload-binary", action="store_true", help="Legacy: instead of uploading results, upload the BINARY (.apk/.aab/.ipa) and have the server RE-SCAN it with its own engine. Implies --upload. Not supported for source directories.")
|
|
2021
|
+
p_scan.add_argument("--upload", action="store_true", help="Submit this scan's RESULTS to your Narvy account for server-side curation (ML false-positive filter, dedup, dashboard history) - the dashboard then shows exactly what this scan found, curated. Works for binaries AND source directories. Requires `narvy login` AND a paid plan (Starter and up); the free/community plan cannot upload (the server returns 403). Local scanning is always free.")
|
|
2022
|
+
p_scan.add_argument("--upload-binary", action="store_true", help="Legacy: instead of uploading results, upload the BINARY (.apk/.aab/.ipa) and have the server RE-SCAN it with its own engine. Implies --upload, so it also needs a paid plan (Starter and up); the free/community plan cannot upload. Not supported for source directories.")
|
|
2008
2023
|
p_scan.add_argument("--confirm-upload", action="store_true",
|
|
2009
2024
|
help="Skip the interactive confirmation when server-side malware triage flags the file as suspicious (severity=warn, e.g. a known intentionally-vulnerable training APK), for CI/non-interactive use. With a real terminal attached, you'll be asked instead. Only ever downgrades a warn, never a hard refuse.")
|
|
2010
2025
|
p_scan.add_argument("--fail-on", choices=["critical", "high", "medium", "low", "any"],
|
|
@@ -2012,7 +2027,7 @@ def cli():
|
|
|
2012
2027
|
p_scan.set_defaults(func=cmd_scan)
|
|
2013
2028
|
|
|
2014
2029
|
p_initci = sub.add_parser(
|
|
2015
|
-
"init-ci",
|
|
2030
|
+
"init-ci", allow_abbrev=False,
|
|
2016
2031
|
help="Write a ready-to-use CI pipeline template (GitHub Actions / GitLab CI / Bitbucket Pipelines) that runs Narvy as a build step",
|
|
2017
2032
|
)
|
|
2018
2033
|
_ci_group = p_initci.add_mutually_exclusive_group()
|
|
@@ -2028,7 +2043,11 @@ def cli():
|
|
|
2028
2043
|
help="Overwrite an existing file without asking (for non-interactive/CI use)")
|
|
2029
2044
|
p_initci.set_defaults(func=cmd_init_ci, platform=None)
|
|
2030
2045
|
|
|
2031
|
-
p_upload_all = sub.add_parser(
|
|
2046
|
+
p_upload_all = sub.add_parser(
|
|
2047
|
+
"upload-all", allow_abbrev=False,
|
|
2048
|
+
help="Scan and upload every .apk/.aab/.apkm/.xapk/.ipa found in a directory to your Narvy account in one command (requires `narvy login` AND a paid plan (Starter and up); the free/community plan cannot upload)",
|
|
2049
|
+
description="Scan and upload every .apk/.aab/.apkm/.xapk/.ipa found in a directory to your Narvy account in one command. Requires `narvy login` AND a paid plan (Starter and up) - the free/community plan cannot upload (the server returns 403). Local scanning with `narvy scan` is always free.",
|
|
2050
|
+
)
|
|
2032
2051
|
p_upload_all.add_argument("directory", help="Directory to search for scan targets")
|
|
2033
2052
|
p_upload_all.add_argument("--no-recursive", action="store_true",
|
|
2034
2053
|
help="Only look in the top-level directory, don't walk subdirectories")
|
|
@@ -2039,7 +2058,10 @@ def cli():
|
|
|
2039
2058
|
help="Skip the interactive confirmation when server-side malware triage flags a file as suspicious (severity=warn), for CI/non-interactive use. With a real terminal attached, you'll be asked per file instead. Only ever downgrades a warn, never a hard refuse.")
|
|
2040
2059
|
p_upload_all.set_defaults(func=cmd_upload_all)
|
|
2041
2060
|
|
|
2042
|
-
|
|
2061
|
+
# allow_abbrev=False: a prefix like `--activ` or `--a` must NOT silently
|
|
2062
|
+
# resolve to `--active` and kick off an active scan the user did not spell
|
|
2063
|
+
# out. An unrecognized flag now errors instead.
|
|
2064
|
+
p_web = sub.add_parser("web-scan", allow_abbrev=False, help="Web vulnerability scan of a URL (Nuclei-based, no account needed). Passive by default; --active runs the full dynamic template set.")
|
|
2043
2065
|
p_web.add_argument("url", help="Target URL to scan")
|
|
2044
2066
|
p_web.add_argument("--active", action="store_true",
|
|
2045
2067
|
help="Dynamic mode: run nuclei's full template set (sends non-GET probes, checks input handling, etc.) instead of the passive GET-only subset. Only scan targets you own or are explicitly authorized to test. Destructive tags (dos, fuzzing, brute-force, intrusive) stay excluded.")
|
|
@@ -2053,7 +2075,7 @@ def cli():
|
|
|
2053
2075
|
p_web.add_argument("--fail-on", choices=["critical", "high", "medium", "low", "any"])
|
|
2054
2076
|
p_web.set_defaults(func=cmd_web_scan)
|
|
2055
2077
|
|
|
2056
|
-
p_host = sub.add_parser("host-audit", help="Agentless Lynis host/infra audit over your own SSH access (no credentials sent to Narvy)")
|
|
2078
|
+
p_host = sub.add_parser("host-audit", allow_abbrev=False, help="Agentless Lynis host/infra audit over your own SSH access (no credentials sent to Narvy)")
|
|
2057
2079
|
p_host.add_argument("host", help="Target hostname or IP")
|
|
2058
2080
|
p_host.add_argument("--user", "-u", required=True, help="SSH username")
|
|
2059
2081
|
p_host.add_argument("--port", "-p", type=int, default=22, help="SSH port (default: 22)")
|
|
@@ -2069,7 +2091,7 @@ def cli():
|
|
|
2069
2091
|
p_host.add_argument("--fail-on", choices=["critical", "high", "medium", "low", "any"])
|
|
2070
2092
|
p_host.set_defaults(func=cmd_host_audit)
|
|
2071
2093
|
|
|
2072
|
-
p_cloud = sub.add_parser("cloud-scan", help="Cloud posture scan using your own ambient credentials (aws configure / env vars / IAM role - zero credential custody)")
|
|
2094
|
+
p_cloud = sub.add_parser("cloud-scan", allow_abbrev=False, help="Cloud posture scan using your own ambient credentials (aws configure / env vars / IAM role - zero credential custody)")
|
|
2073
2095
|
p_cloud.add_argument("--provider", choices=["aws"], default="aws", help="Cloud provider (GCP/Azure not yet in the CLI)")
|
|
2074
2096
|
p_cloud.add_argument("--region", help="Specific region to scan (default: all regions)")
|
|
2075
2097
|
p_cloud.add_argument("--output", "-o", "--format", dest="format", choices=["text", "json", "sarif"],
|
|
@@ -217,3 +217,118 @@ rules:
|
|
|
217
217
|
metavariable: $MSG
|
|
218
218
|
patterns:
|
|
219
219
|
- pattern-not-regex: '^"[^"]*"$'
|
|
220
|
+
|
|
221
|
+
# exec()/ProcessBuilder command built by string concatenation (pure-literal concats suppressed).
|
|
222
|
+
- id: android-command-injection-runtime-exec
|
|
223
|
+
languages: [java, kotlin]
|
|
224
|
+
severity: ERROR
|
|
225
|
+
message: >-
|
|
226
|
+
A shell/process command is built with string concatenation and passed to
|
|
227
|
+
Runtime.exec()/ProcessBuilder. If any concatenated operand is
|
|
228
|
+
user-controlled this is command injection (CWE-78). Pass a fixed program
|
|
229
|
+
and its arguments as a String[] / argument list, never a concatenated
|
|
230
|
+
command line, and validate any dynamic argument against an allow-list.
|
|
231
|
+
metadata:
|
|
232
|
+
cwe: CWE-78
|
|
233
|
+
masvs: MSTG-PLATFORM-2
|
|
234
|
+
category: injection
|
|
235
|
+
confidence: LOW
|
|
236
|
+
patterns:
|
|
237
|
+
- pattern-either:
|
|
238
|
+
- pattern: $RT.exec($A + $B, ...)
|
|
239
|
+
- pattern: new ProcessBuilder($A + $B, ...)
|
|
240
|
+
- pattern: new ProcessBuilder().command($A + $B, ...)
|
|
241
|
+
- pattern-not:
|
|
242
|
+
patterns:
|
|
243
|
+
- pattern-either:
|
|
244
|
+
- pattern: $RT.exec($A + $B, ...)
|
|
245
|
+
- pattern: new ProcessBuilder($A + $B, ...)
|
|
246
|
+
- pattern: new ProcessBuilder().command($A + $B, ...)
|
|
247
|
+
- metavariable-pattern:
|
|
248
|
+
metavariable: $A
|
|
249
|
+
patterns:
|
|
250
|
+
- pattern-regex: (?s)^(?:\s|\+)*(?:"[^"]*"|[A-Za-z_][A-Za-z0-9_.]*\.[A-Z][A-Z0-9_]*|[A-Z][A-Z0-9_]*)(?:(?:\s|\+)+(?:"[^"]*"|[A-Za-z_][A-Za-z0-9_.]*\.[A-Z][A-Z0-9_]*|[A-Z][A-Z0-9_]*))*(?:\s|\+)*$
|
|
251
|
+
- metavariable-pattern:
|
|
252
|
+
metavariable: $B
|
|
253
|
+
patterns:
|
|
254
|
+
- pattern-regex: (?s)^(?:\s|\+)*(?:"[^"]*"|[A-Za-z_][A-Za-z0-9_.]*\.[A-Z][A-Z0-9_]*|[A-Z][A-Z0-9_]*)(?:(?:\s|\+)+(?:"[^"]*"|[A-Za-z_][A-Za-z0-9_.]*\.[A-Z][A-Z0-9_]*|[A-Z][A-Z0-9_]*))*(?:\s|\+)*$
|
|
255
|
+
|
|
256
|
+
# exec/ProcessBuilder given an argument array or arg list with a non-literal element;
|
|
257
|
+
# fully-literal arrays/lists (every element a string literal) are left alone.
|
|
258
|
+
- id: android-command-injection-array
|
|
259
|
+
languages: [java, kotlin]
|
|
260
|
+
severity: ERROR
|
|
261
|
+
message: >-
|
|
262
|
+
A shell/process command is passed to Runtime.exec()/ProcessBuilder as an
|
|
263
|
+
argument array (or arg list) in which at least one element is not a string
|
|
264
|
+
literal. If that element is user-controlled this is command injection
|
|
265
|
+
(CWE-78). Validate every dynamic argument against an allow-list, and never
|
|
266
|
+
pass unchecked input as an argument to a shell such as sh -c.
|
|
267
|
+
metadata:
|
|
268
|
+
cwe: CWE-78
|
|
269
|
+
masvs: MSTG-PLATFORM-2
|
|
270
|
+
category: injection
|
|
271
|
+
confidence: LOW
|
|
272
|
+
patterns:
|
|
273
|
+
- pattern-either:
|
|
274
|
+
- pattern: $RT.exec(new String[]{..., $X, ...})
|
|
275
|
+
- pattern: new ProcessBuilder(new String[]{..., $X, ...})
|
|
276
|
+
- pattern: $PB.command(new String[]{..., $X, ...})
|
|
277
|
+
- pattern: new ProcessBuilder(Arrays.asList(..., $X, ...))
|
|
278
|
+
- pattern: new ProcessBuilder(List.of(..., $X, ...))
|
|
279
|
+
- pattern: $PB.command(Arrays.asList(..., $X, ...))
|
|
280
|
+
- pattern: $PB.command(List.of(..., $X, ...))
|
|
281
|
+
- pattern: new ProcessBuilder(..., $X, ...)
|
|
282
|
+
- pattern: $PB.command(..., $X, ...)
|
|
283
|
+
- metavariable-regex:
|
|
284
|
+
metavariable: $X
|
|
285
|
+
regex: '^(?!"[^"]*"$)(?!new )(?!Arrays\.)(?!List\.)(?!Collections\.)(?!Set\.)(?!ImmutableList\.)(?!Lists\.)(?!R\.).+'
|
|
286
|
+
|
|
287
|
+
# Credential-named variable assigned a hardcoded literal (name-only vars and placeholders excluded); split java/kotlin for parse compatibility.
|
|
288
|
+
- id: android-hardcoded-credential-java
|
|
289
|
+
languages: [java]
|
|
290
|
+
severity: ERROR
|
|
291
|
+
message: >-
|
|
292
|
+
A credential-like variable (password / token / credential) is assigned a
|
|
293
|
+
hardcoded string literal. Anyone who decompiles the APK recovers this
|
|
294
|
+
value. Do not embed credentials in code - load them from the Android
|
|
295
|
+
Keystore / EncryptedSharedPreferences, or fetch them from a backend at
|
|
296
|
+
runtime.
|
|
297
|
+
metadata:
|
|
298
|
+
cwe: CWE-798
|
|
299
|
+
masvs: MSTG-STORAGE-1
|
|
300
|
+
category: secrets
|
|
301
|
+
patterns:
|
|
302
|
+
- pattern-either:
|
|
303
|
+
- pattern: $T $VAR = $VAL;
|
|
304
|
+
- pattern: $VAR = $VAL;
|
|
305
|
+
- metavariable-regex:
|
|
306
|
+
metavariable: $VAR
|
|
307
|
+
regex: (?i)^(?!.*(?:name|field|header|label|title|url|uri|path|column|type|action|extra|pref|prefs|hint|key)$)[a-z0-9_]*(?:password|passwd|pwd|apitoken|access_?token|auth_?token|credential|secret_?token|sessiontoken)[a-z0-9_]*$
|
|
308
|
+
- metavariable-regex:
|
|
309
|
+
metavariable: $VAL
|
|
310
|
+
regex: ^"(?!.{0,5}")(?!.*(?i:example|sample|test|fake|placeholder|dummy|changeme|your_|replace|xxxx|todo|fixme))(?!https?://)[^"]{6,}"$
|
|
311
|
+
|
|
312
|
+
- id: android-hardcoded-credential-kotlin
|
|
313
|
+
languages: [kotlin]
|
|
314
|
+
severity: ERROR
|
|
315
|
+
message: >-
|
|
316
|
+
A credential-like variable (password / token / credential) is assigned a
|
|
317
|
+
hardcoded string literal. Anyone who decompiles the APK recovers this
|
|
318
|
+
value. Do not embed credentials in code - load them from the Android
|
|
319
|
+
Keystore / EncryptedSharedPreferences, or fetch them from a backend at
|
|
320
|
+
runtime.
|
|
321
|
+
metadata:
|
|
322
|
+
cwe: CWE-798
|
|
323
|
+
masvs: MSTG-STORAGE-1
|
|
324
|
+
category: secrets
|
|
325
|
+
patterns:
|
|
326
|
+
- pattern-either:
|
|
327
|
+
- pattern: val $VAR = $VAL
|
|
328
|
+
- pattern: var $VAR = $VAL
|
|
329
|
+
- metavariable-regex:
|
|
330
|
+
metavariable: $VAR
|
|
331
|
+
regex: (?i)^(?!.*(?:name|field|header|label|title|url|uri|path|column|type|action|extra|pref|prefs|hint|key)$)[a-z0-9_]*(?:password|passwd|pwd|apitoken|access_?token|auth_?token|credential|secret_?token|sessiontoken)[a-z0-9_]*$
|
|
332
|
+
- metavariable-regex:
|
|
333
|
+
metavariable: $VAL
|
|
334
|
+
regex: ^"(?!.{0,5}")(?!.*(?i:example|sample|test|fake|placeholder|dummy|changeme|your_|replace|xxxx|todo|fixme))(?!https?://)[^"]{6,}"$
|
|
@@ -30,7 +30,7 @@ rules:
|
|
|
30
30
|
- id: ios-swift-weak-hash
|
|
31
31
|
languages: [swift]
|
|
32
32
|
severity: WARNING
|
|
33
|
-
message: "MD5/SHA-1 are broken for security-sensitive hashing (CommonCrypto CC_MD5/CC_SHA1
|
|
33
|
+
message: "MD5/SHA-1 are broken for security-sensitive hashing (CommonCrypto CC_MD5/CC_SHA1, CryptoKit Insecure.MD5/Insecure.SHA1, or a CryptoSwift .md5()/.sha1() call). Use SHA256+."
|
|
34
34
|
metadata:
|
|
35
35
|
cwe: CWE-328
|
|
36
36
|
masvs: MSTG-CRYPTO-4
|
|
@@ -42,6 +42,9 @@ rules:
|
|
|
42
42
|
- pattern: Insecure.SHA1.hash(...)
|
|
43
43
|
- pattern: Insecure.MD5()
|
|
44
44
|
- pattern: Insecure.SHA1()
|
|
45
|
+
# CryptoSwift adds .md5()/.sha1() extensions to String and Array<UInt8>.
|
|
46
|
+
- pattern: $X.md5()
|
|
47
|
+
- pattern: $X.sha1()
|
|
45
48
|
|
|
46
49
|
- id: ios-swift-weak-prng
|
|
47
50
|
languages: [swift]
|
|
@@ -307,6 +310,36 @@ rules:
|
|
|
307
310
|
category: secrets
|
|
308
311
|
pattern-regex: '"https?://[^/\s:"]+:[^/\s@"]+@'
|
|
309
312
|
|
|
313
|
+
- id: ios-swift-hardcoded-secret-assignment
|
|
314
|
+
languages: [swift]
|
|
315
|
+
severity: ERROR
|
|
316
|
+
message: >-
|
|
317
|
+
A credential-like property (password / apiKey / clientSecret / auth
|
|
318
|
+
token) is assigned a hardcoded string literal. Anyone who inspects the
|
|
319
|
+
app bundle recovers this value. Load it from the Keychain or fetch it
|
|
320
|
+
from a backend at runtime instead of embedding it in source.
|
|
321
|
+
metadata:
|
|
322
|
+
cwe: CWE-798
|
|
323
|
+
masvs: MSTG-STORAGE-1
|
|
324
|
+
category: secrets
|
|
325
|
+
patterns:
|
|
326
|
+
# Only a string LITERAL initialiser; values read from env, Bundle, or a
|
|
327
|
+
# function call are not literals and never match.
|
|
328
|
+
- pattern-either:
|
|
329
|
+
- pattern: let $VAR = $VAL
|
|
330
|
+
- pattern: var $VAR = $VAL
|
|
331
|
+
# The property name must signal a secret, and must NOT end in a word that
|
|
332
|
+
# marks it as a key/field NAME rather than a secret VALUE (headerName,
|
|
333
|
+
# apiKeyName, secretHeaderField, ...).
|
|
334
|
+
- metavariable-regex:
|
|
335
|
+
metavariable: $VAR
|
|
336
|
+
regex: (?i)^(?!.*(?:name|field|header|label|title|url|uri|path|column|type|action|hint)$)[a-z0-9_]*(?:password|passwd|pwd|apikey|api_key|clientsecret|client_secret|access_?token|auth_?token|authtoken|credential|secret_?token)[a-z0-9_]*$
|
|
337
|
+
# Value must be a non-trivial literal (>= 8 chars) and not an obvious
|
|
338
|
+
# placeholder or a URL.
|
|
339
|
+
- metavariable-regex:
|
|
340
|
+
metavariable: $VAL
|
|
341
|
+
regex: ^"(?!.{0,7}")(?!.*(?i:example|sample|test|fake|placeholder|dummy|changeme|your_|replace|xxxx|todo|fixme))(?!https?://)[^"]{8,}"$
|
|
342
|
+
|
|
310
343
|
- id: ios-swift-userdefaults-sensitive-data
|
|
311
344
|
languages: [swift]
|
|
312
345
|
severity: WARNING
|
|
@@ -83,18 +83,18 @@ def degraded_coverage_note() -> Optional[str]:
|
|
|
83
83
|
return None
|
|
84
84
|
if LAST_RUN.get('status') == 'timeout':
|
|
85
85
|
return (
|
|
86
|
-
"
|
|
87
|
-
f"
|
|
88
|
-
f"
|
|
89
|
-
"
|
|
90
|
-
"
|
|
91
|
-
"
|
|
92
|
-
"
|
|
86
|
+
"The regex, taint and SCA passes completed here with full results. "
|
|
87
|
+
f"The deep structural (Semgrep) pass is time-bounded to "
|
|
88
|
+
f"{LAST_RUN.get('timeout_s')}s locally and this target is large "
|
|
89
|
+
f"({LAST_RUN.get('file_count')} files); the hosted engine runs that "
|
|
90
|
+
"pass to completion with no local time budget - use `narvy scan "
|
|
91
|
+
"--upload` for the full structural pass, or raise "
|
|
92
|
+
"NARVY_SEMGREP_TIMEOUT / narrow the path to finish it locally."
|
|
93
93
|
)
|
|
94
94
|
return (
|
|
95
|
-
"
|
|
96
|
-
|
|
97
|
-
"'
|
|
95
|
+
"The regex, taint and SCA passes completed here with full results. The "
|
|
96
|
+
"deep structural (Semgrep) pass runs to completion on the hosted engine "
|
|
97
|
+
f"({LAST_RUN.get('message')}) - use `narvy scan --upload` for it."
|
|
98
98
|
)
|
|
99
99
|
|
|
100
100
|
|
|
@@ -394,9 +394,9 @@ class NucleiScanner:
|
|
|
394
394
|
f"target is rate-limiting/blocking the scanner - {len(probe['blocked'])} "
|
|
395
395
|
f"of {len(probe['statuses'])} verification requests to {scan_target} came "
|
|
396
396
|
f"back HTTP {'/'.join(str(s) for s in sorted(set(probe['blocked'])))}. "
|
|
397
|
-
f"
|
|
398
|
-
f"
|
|
399
|
-
f"with a lower --rate-limit
|
|
397
|
+
f"This run could not reach real content, so it is inconclusive. Run it "
|
|
398
|
+
f"from the Narvy platform (allowlisted egress + the full active engine), "
|
|
399
|
+
f"or re-run with a lower --rate-limit."
|
|
400
400
|
)
|
|
401
401
|
out["degraded"] = True
|
|
402
402
|
out["degraded_reason"] = reason
|
|
@@ -408,10 +408,10 @@ class NucleiScanner:
|
|
|
408
408
|
f"target never answered - all {self._RATE_LIMIT_PROBES} verification "
|
|
409
409
|
f"requests to {scan_target} failed to get any HTTP response at all "
|
|
410
410
|
f"(connection refused/timed out/DNS or TLS failure). Nuclei's requests "
|
|
411
|
-
f"were dropped the same way, so nothing was
|
|
412
|
-
f"
|
|
413
|
-
f"
|
|
414
|
-
f"and port are right."
|
|
411
|
+
f"were dropped the same way, so nothing was scanned and this run is "
|
|
412
|
+
f"inconclusive. Run it from the Narvy platform (different network "
|
|
413
|
+
f"egress), or check the host is up and reachable from this machine "
|
|
414
|
+
f"(proxy/firewall/egress rules), and that the scheme and port are right."
|
|
415
415
|
)
|
|
416
416
|
out["degraded"] = True
|
|
417
417
|
out["degraded_reason"] = reason
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
# Narvy SAST - free security scanning in Bitbucket Pipelines.
|
|
2
|
-
# `narvy init-ci --bitbucket` writes this file. Safe to edit and commit.
|
|
3
|
-
#
|
|
4
|
-
# If you ALREADY have a bitbucket-pipelines.yml, do NOT overwrite it - copy the
|
|
5
|
-
# step below into your existing pipeline instead.
|
|
6
|
-
#
|
|
7
|
-
# What it does: runs the Narvy CLI over the repository, emits SARIF
|
|
8
|
-
# (stored as a build artifact), and fails the build when a finding is at or
|
|
9
|
-
# above the severity you choose with --fail-on.
|
|
10
|
-
#
|
|
11
|
-
# NOTE: to surface findings in Bitbucket's Code Insights panel you would POST
|
|
12
|
-
# the SARIF via the Bitbucket Reports API (an extra step / pipe), which this
|
|
13
|
-
# template does not do. The build gate (--fail-on) works as-is.
|
|
14
|
-
image: python:3.11
|
|
15
|
-
|
|
16
|
-
pipelines:
|
|
17
|
-
default:
|
|
18
|
-
- step:
|
|
19
|
-
name: Narvy SAST
|
|
20
|
-
script:
|
|
21
|
-
- pip install --upgrade pip
|
|
22
|
-
- pip install narvy
|
|
23
|
-
- narvy scan . --output sarif --file narvy-results.sarif --fail-on high
|
|
24
|
-
artifacts:
|
|
25
|
-
- narvy-results.sarif
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
# Narvy SAST - free security scanning in GitHub Actions.
|
|
2
|
-
# `narvy init-ci --github` writes this file. Safe to edit and commit.
|
|
3
|
-
#
|
|
4
|
-
# What it does: runs the Narvy CLI over this repository (mobile / web /
|
|
5
|
-
# backend source SAST + SCA), emits SARIF, uploads it to GitHub code scanning
|
|
6
|
-
# (the repo's Security tab), and fails the build when a finding is at or above
|
|
7
|
-
# the severity you choose with --fail-on.
|
|
8
|
-
name: Narvy SAST
|
|
9
|
-
|
|
10
|
-
on:
|
|
11
|
-
push:
|
|
12
|
-
branches: [ main, master ]
|
|
13
|
-
pull_request:
|
|
14
|
-
workflow_dispatch:
|
|
15
|
-
|
|
16
|
-
permissions:
|
|
17
|
-
contents: read
|
|
18
|
-
security-events: write # required to upload SARIF to code scanning
|
|
19
|
-
|
|
20
|
-
jobs:
|
|
21
|
-
narvy:
|
|
22
|
-
runs-on: ubuntu-latest
|
|
23
|
-
steps:
|
|
24
|
-
- uses: actions/checkout@v4
|
|
25
|
-
|
|
26
|
-
- uses: actions/setup-python@v5
|
|
27
|
-
with:
|
|
28
|
-
python-version: '3.11'
|
|
29
|
-
|
|
30
|
-
- name: Install Narvy CLI
|
|
31
|
-
run: |
|
|
32
|
-
python -m pip install --upgrade pip
|
|
33
|
-
pip install narvy
|
|
34
|
-
|
|
35
|
-
- name: Run Narvy scan
|
|
36
|
-
# The SARIF file is written before the --fail-on gate runs, so the
|
|
37
|
-
# Security tab is populated even on a failing build (see the upload
|
|
38
|
-
# step's `if: always()`).
|
|
39
|
-
run: |
|
|
40
|
-
narvy scan . \
|
|
41
|
-
--output sarif \
|
|
42
|
-
--file narvy-results.sarif \
|
|
43
|
-
--fail-on high
|
|
44
|
-
|
|
45
|
-
- name: Upload SARIF to GitHub code scanning
|
|
46
|
-
if: always() # upload even when the scan step failed the build via --fail-on
|
|
47
|
-
uses: github/codeql-action/upload-sarif@v3
|
|
48
|
-
with:
|
|
49
|
-
sarif_file: narvy-results.sarif
|
|
50
|
-
|
|
51
|
-
# ---------------------------------------------------------------------------
|
|
52
|
-
# Scanning a web app URL too? Add a second step (needs no account):
|
|
53
|
-
# - name: Narvy passive web scan
|
|
54
|
-
# run: narvy web-scan "https://staging.example.com" --format sarif --output-file web.sarif --fail-on high
|
|
55
|
-
# - if: always()
|
|
56
|
-
# uses: github/codeql-action/upload-sarif@v3
|
|
57
|
-
# with:
|
|
58
|
-
# sarif_file: web.sarif
|
|
59
|
-
# category: narvy-web
|
|
60
|
-
# ---------------------------------------------------------------------------
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
# Narvy SAST - free security scanning in GitLab CI.
|
|
2
|
-
# `narvy init-ci --gitlab` writes this file. Safe to edit and commit.
|
|
3
|
-
#
|
|
4
|
-
# If you ALREADY have a .gitlab-ci.yml, do NOT overwrite it - copy the
|
|
5
|
-
# `narvy_sast` job below into your existing pipeline instead.
|
|
6
|
-
#
|
|
7
|
-
# What it does: runs the Narvy CLI over the repository, emits SARIF,
|
|
8
|
-
# saves it as a job artifact, and fails the pipeline when a finding is at or
|
|
9
|
-
# above the severity you choose with --fail-on.
|
|
10
|
-
#
|
|
11
|
-
# NOTE on GitLab's Security Dashboard: native ingestion
|
|
12
|
-
# (artifacts:reports:sast) expects GitLab's OWN SAST report schema, which this
|
|
13
|
-
# CLI does not emit. The SARIF is stored as a plain downloadable artifact and
|
|
14
|
-
# the pipeline gate (--fail-on) still blocks the MR. Surfacing findings inside
|
|
15
|
-
# the Security tab would require converting SARIF -> GitLab SAST schema, which
|
|
16
|
-
# this template does not do.
|
|
17
|
-
stages:
|
|
18
|
-
- test
|
|
19
|
-
|
|
20
|
-
narvy_sast:
|
|
21
|
-
stage: test
|
|
22
|
-
image: python:3.11-slim
|
|
23
|
-
before_script:
|
|
24
|
-
- pip install --upgrade pip
|
|
25
|
-
- pip install narvy
|
|
26
|
-
script:
|
|
27
|
-
- narvy scan . --output sarif --file narvy-results.sarif --fail-on high
|
|
28
|
-
artifacts:
|
|
29
|
-
when: always # keep the SARIF even when --fail-on failed the pipeline
|
|
30
|
-
paths:
|
|
31
|
-
- narvy-results.sarif
|
|
32
|
-
expire_in: 1 week
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|