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.
Files changed (105) hide show
  1. {narvy_cli-1.0.1/narvy_cli.egg-info → narvy_cli-1.0.2}/PKG-INFO +1 -1
  2. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/__init__.py +1 -1
  3. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/apk_memory_preflight.py +3 -3
  4. narvy_cli-1.0.2/narvy/ci_templates/bitbucket.yml +15 -0
  5. narvy_cli-1.0.2/narvy/ci_templates/github.yml +48 -0
  6. narvy_cli-1.0.2/narvy/ci_templates/gitlab.yml +19 -0
  7. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/doctor.py +6 -6
  8. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/ios/binary_analyzer.py +10 -20
  9. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/main.py +46 -24
  10. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/rules/android.yml +115 -0
  11. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/rules/ios_swift.yml +34 -1
  12. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/semgrep_engine.py +10 -10
  13. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/web/scanner.py +7 -7
  14. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/web/source_analyzer.py +1 -1
  15. {narvy_cli-1.0.1 → narvy_cli-1.0.2/narvy_cli.egg-info}/PKG-INFO +1 -1
  16. narvy_cli-1.0.1/narvy/ci_templates/bitbucket.yml +0 -25
  17. narvy_cli-1.0.1/narvy/ci_templates/github.yml +0 -60
  18. narvy_cli-1.0.1/narvy/ci_templates/gitlab.yml +0 -32
  19. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/CHANGELOG.md +0 -0
  20. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/LICENSE +0 -0
  21. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/MANIFEST.in +0 -0
  22. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/README.md +0 -0
  23. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/SECURITY.md +0 -0
  24. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/android/__init__.py +0 -0
  25. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/android/source_analyzer.py +0 -0
  26. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/android/split_bundle.py +0 -0
  27. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/android_rule_context.py +0 -0
  28. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/auth.py +0 -0
  29. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/cloud/__init__.py +0 -0
  30. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/cloud/aws_scan.py +0 -0
  31. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/comment_filter.py +0 -0
  32. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/crypto_taint_lite.py +0 -0
  33. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/decompiler.py +0 -0
  34. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/host/__init__.py +0 -0
  35. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/host/audit.py +0 -0
  36. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/host/host_knowledge.py +0 -0
  37. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/host/lynis_bootstrap.py +0 -0
  38. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/host/lynis_parser.py +0 -0
  39. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/host/narvy_checks.py +0 -0
  40. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/host/report.py +0 -0
  41. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/host/ssh_exec.py +0 -0
  42. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/ios/__init__.py +0 -0
  43. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/ios/plist_checks.py +0 -0
  44. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/ios/source_analyzer.py +0 -0
  45. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/ios/third_party_filter.py +0 -0
  46. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/ios/trust_all_context.py +0 -0
  47. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/native_hardening.py +0 -0
  48. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/reporter.py +0 -0
  49. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/rule_engine.py +0 -0
  50. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/rules/android/config.yml +0 -0
  51. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/rules/android/crypto.yml +0 -0
  52. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/rules/android/secrets.yml +0 -0
  53. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/rules/android/storage.yml +0 -0
  54. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/rules/android/webview.yml +0 -0
  55. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/rules/ios/objc/crypto.yml +0 -0
  56. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/rules/ios/objc/network.yml +0 -0
  57. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/rules/ios/objc/secrets.yml +0 -0
  58. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/rules/ios/objc/storage.yml +0 -0
  59. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/rules/ios/objc/webview.yml +0 -0
  60. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/rules/web/go.yml +0 -0
  61. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/rules/web/java.yml +0 -0
  62. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/rules/web/javascript.yml +0 -0
  63. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/rules/web/kotlin.yml +0 -0
  64. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/rules/web/local/csharp_narvy/config.yml +0 -0
  65. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/rules/web/local/csharp_narvy/crypto.yml +0 -0
  66. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/rules/web/local/csharp_narvy/deserialization.yml +0 -0
  67. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/rules/web/local/csharp_narvy/injection.yml +0 -0
  68. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/rules/web/local/csharp_narvy/xxe.yml +0 -0
  69. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/rules/web/local/java_narvy/auth_jwt.yml +0 -0
  70. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/rules/web/local/java_narvy/deserialization.yml +0 -0
  71. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/rules/web/local/java_narvy/mybatis.yml +0 -0
  72. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/rules/web/local/java_narvy/snakeyaml.yml +0 -0
  73. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/rules/web/local/java_narvy/spring_authz.yml +0 -0
  74. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/rules/web/local/java_narvy/spring_config.yml +0 -0
  75. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/rules/web/local/java_narvy/spring_hardening.yml +0 -0
  76. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/rules/web/local/java_narvy/sqli.yml +0 -0
  77. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/rules/web/local/java_narvy/xxe.yml +0 -0
  78. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/rules/web/php.yml +0 -0
  79. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/rules/web/python.yml +0 -0
  80. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/rules/web/ruby.yml +0 -0
  81. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/rules/web/rust.yml +0 -0
  82. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/rules/web/secrets.yml +0 -0
  83. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/rules/web/secrets_supplement.yml +0 -0
  84. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/sca/__init__.py +0 -0
  85. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/sca/android_deps.py +0 -0
  86. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/sca/ios_deps.py +0 -0
  87. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/sca/osv_client.py +0 -0
  88. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/sca/web_deps.py +0 -0
  89. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/scope_config.py +0 -0
  90. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/stack_protector_evidence.py +0 -0
  91. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/third_party_filter.py +0 -0
  92. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/uploader.py +0 -0
  93. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/weak_prng_context.py +0 -0
  94. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/web/__init__.py +0 -0
  95. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/web/nuclei_binary.py +0 -0
  96. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/web/scan_blocklist.py +0 -0
  97. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy/web/ssrf_guard.py +0 -0
  98. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy_cli.egg-info/SOURCES.txt +0 -0
  99. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy_cli.egg-info/dependency_links.txt +0 -0
  100. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy_cli.egg-info/entry_points.txt +0 -0
  101. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy_cli.egg-info/requires.txt +0 -0
  102. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/narvy_cli.egg-info/top_level.txt +0 -0
  103. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/pyproject.toml +0 -0
  104. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/setup.cfg +0 -0
  105. {narvy_cli-1.0.1 → narvy_cli-1.0.2}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: narvy-cli
3
- Version: 1.0.1
3
+ Version: 1.0.2
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
@@ -1,3 +1,3 @@
1
1
  """Narvy CLI - free, local SAST for Android, iOS and polyglot source."""
2
2
 
3
- __version__ = "1.0.1"
3
+ __version__ = "1.0.2"
@@ -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" - free up that much memory (close other apps), then re-run with --max-mem {suggest_gb}g\n"
213
- f" - scan this app on a bigger machine, or use the hosted scan (`--upload`)\n"
214
- f" - run it anyway with --force if you know your setup can take it"
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 (large production app scale) need 8-24 GB of free "
114
- "memory to decompile. `narvy scan` will tell you before it starts, and "
115
- "won't waste your time failing halfway - but on this machine expect to be "
116
- "limited to smaller apps, or use `--upload` for the hosted scan."
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's App Store DRM, cryptid=1): "
1025
- "the executable __TEXT code is ciphertext until the device that "
1026
- "purchased/installed it decrypts it at launch time using a key Apple "
1027
- "grants only to that device - no static analysis tool, Narvy "
1028
- "included, can read the actual instruction stream of an encrypted "
1029
- "App Store export. This is a physical limitation of the distribution "
1030
- "format, not a gap in this scanner. What still works and was still "
1031
- "run: Info.plist / entitlements / URL-scheme / ATS analysis (always "
1032
- "cleartext), linked-framework and embedded-framework-name listing "
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> --upload` to submit one."
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 found among what could be analyzed on this "
1093
- "FairPlay-encrypted binary (see the encryption notice above) - this is "
1094
- "NOT the same as a clean bill of health.[/bold yellow]"
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 our full polyglot web SAST engine (more languages/rule packs, SDLC/compliance mapping, ML triage).")
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 our full iOS source SAST engine (deeper Swift/ObjC semantic analysis, MASVS mapping, ML triage).")
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, so the rules that read code and string "
1274
- "constants were structurally blind to most of the binary (see the "
1275
- "encryption notice above)."
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` first.")
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("upload-all", help="Scan and upload every .apk/.aab/.apkm/.xapk/.ipa found in a directory to your Narvy account in one command (requires `narvy login` first)")
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
- p_web = sub.add_parser("web-scan", help="Web vulnerability scan of a URL (Nuclei-based, no account needed). Passive by default; --active runs the full dynamic template set.")
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 or CryptoKit Insecure.MD5/Insecure.SHA1). Use SHA256+."
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
- "PARTIAL COVERAGE: the deep structural (Semgrep) pass did NOT "
87
- f"finish within its {LAST_RUN.get('timeout_s')}s budget on this "
88
- f"target ({LAST_RUN.get('file_count')} files). Zero findings from "
89
- "that pass means 'not fully scanned', NOT 'clean'. The regex, "
90
- "taint and SCA passes still ran to completion and their findings "
91
- "are complete. Re-run with NARVY_SEMGREP_TIMEOUT set higher "
92
- "(seconds), or scan a narrower path."
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
- "PARTIAL COVERAGE: the deep structural (Semgrep) pass failed "
96
- f"({LAST_RUN.get('message')}). Zero findings from that pass means "
97
- "'not fully scanned', NOT 'clean'."
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"Nuclei's requests were throttled the same way, so this scan did not "
398
- f"reach real content: 0 findings here means UNKNOWN, not clean. Re-run "
399
- f"with a lower --rate-limit, or from an allowlisted source IP."
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 ever scanned: 0 findings "
412
- f"here means UNREACHABLE, not clean. Check the host is up and reachable "
413
- f"from this machine (proxy/firewall/egress rules), and that the scheme "
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,5 +1,5 @@
1
1
  """Web/backend source-repo SAST entry point for JS/TS, Python, PHP, Go, Ruby,
2
- Rust, Java/Kotlin and C#/.NET, driving vendored semgrep rule packs.
2
+ Rust, Java/Kotlin and C#/.NET, driving the bundled semgrep rule packs.
3
3
  """
4
4
  from __future__ import annotations
5
5
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: narvy-cli
3
- Version: 1.0.1
3
+ Version: 1.0.2
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
@@ -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