narvy-cli 1.0.1__tar.gz → 1.0.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. {narvy_cli-1.0.1/narvy_cli.egg-info → narvy_cli-1.0.3}/PKG-INFO +1 -1
  2. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/__init__.py +1 -1
  3. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/android/source_analyzer.py +99 -0
  4. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/apk_memory_preflight.py +3 -3
  5. narvy_cli-1.0.3/narvy/ci_templates/bitbucket.yml +15 -0
  6. narvy_cli-1.0.3/narvy/ci_templates/github.yml +48 -0
  7. narvy_cli-1.0.3/narvy/ci_templates/gitlab.yml +19 -0
  8. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/doctor.py +6 -6
  9. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/ios/binary_analyzer.py +10 -20
  10. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/main.py +304 -43
  11. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/rules/android.yml +115 -0
  12. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/rules/ios_swift.yml +34 -1
  13. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/rules/web/python.yml +3 -5
  14. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/rules/web/secrets.yml +9 -0
  15. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/rules/web/secrets_supplement.yml +8 -4
  16. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/sca/web_deps.py +205 -4
  17. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/semgrep_engine.py +10 -10
  18. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/web/scanner.py +7 -7
  19. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/web/source_analyzer.py +277 -55
  20. {narvy_cli-1.0.1 → narvy_cli-1.0.3/narvy_cli.egg-info}/PKG-INFO +1 -1
  21. narvy_cli-1.0.1/narvy/ci_templates/bitbucket.yml +0 -25
  22. narvy_cli-1.0.1/narvy/ci_templates/github.yml +0 -60
  23. narvy_cli-1.0.1/narvy/ci_templates/gitlab.yml +0 -32
  24. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/CHANGELOG.md +0 -0
  25. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/LICENSE +0 -0
  26. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/MANIFEST.in +0 -0
  27. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/README.md +0 -0
  28. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/SECURITY.md +0 -0
  29. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/android/__init__.py +0 -0
  30. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/android/split_bundle.py +0 -0
  31. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/android_rule_context.py +0 -0
  32. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/auth.py +0 -0
  33. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/cloud/__init__.py +0 -0
  34. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/cloud/aws_scan.py +0 -0
  35. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/comment_filter.py +0 -0
  36. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/crypto_taint_lite.py +0 -0
  37. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/decompiler.py +0 -0
  38. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/host/__init__.py +0 -0
  39. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/host/audit.py +0 -0
  40. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/host/host_knowledge.py +0 -0
  41. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/host/lynis_bootstrap.py +0 -0
  42. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/host/lynis_parser.py +0 -0
  43. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/host/narvy_checks.py +0 -0
  44. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/host/report.py +0 -0
  45. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/host/ssh_exec.py +0 -0
  46. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/ios/__init__.py +0 -0
  47. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/ios/plist_checks.py +0 -0
  48. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/ios/source_analyzer.py +0 -0
  49. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/ios/third_party_filter.py +0 -0
  50. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/ios/trust_all_context.py +0 -0
  51. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/native_hardening.py +0 -0
  52. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/reporter.py +0 -0
  53. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/rule_engine.py +0 -0
  54. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/rules/android/config.yml +0 -0
  55. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/rules/android/crypto.yml +0 -0
  56. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/rules/android/secrets.yml +0 -0
  57. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/rules/android/storage.yml +0 -0
  58. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/rules/android/webview.yml +0 -0
  59. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/rules/ios/objc/crypto.yml +0 -0
  60. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/rules/ios/objc/network.yml +0 -0
  61. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/rules/ios/objc/secrets.yml +0 -0
  62. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/rules/ios/objc/storage.yml +0 -0
  63. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/rules/ios/objc/webview.yml +0 -0
  64. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/rules/web/go.yml +0 -0
  65. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/rules/web/java.yml +0 -0
  66. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/rules/web/javascript.yml +0 -0
  67. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/rules/web/kotlin.yml +0 -0
  68. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/rules/web/local/csharp_narvy/config.yml +0 -0
  69. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/rules/web/local/csharp_narvy/crypto.yml +0 -0
  70. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/rules/web/local/csharp_narvy/deserialization.yml +0 -0
  71. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/rules/web/local/csharp_narvy/injection.yml +0 -0
  72. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/rules/web/local/csharp_narvy/xxe.yml +0 -0
  73. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/rules/web/local/java_narvy/auth_jwt.yml +0 -0
  74. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/rules/web/local/java_narvy/deserialization.yml +0 -0
  75. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/rules/web/local/java_narvy/mybatis.yml +0 -0
  76. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/rules/web/local/java_narvy/snakeyaml.yml +0 -0
  77. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/rules/web/local/java_narvy/spring_authz.yml +0 -0
  78. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/rules/web/local/java_narvy/spring_config.yml +0 -0
  79. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/rules/web/local/java_narvy/spring_hardening.yml +0 -0
  80. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/rules/web/local/java_narvy/sqli.yml +0 -0
  81. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/rules/web/local/java_narvy/xxe.yml +0 -0
  82. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/rules/web/php.yml +0 -0
  83. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/rules/web/ruby.yml +0 -0
  84. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/rules/web/rust.yml +0 -0
  85. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/sca/__init__.py +0 -0
  86. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/sca/android_deps.py +0 -0
  87. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/sca/ios_deps.py +0 -0
  88. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/sca/osv_client.py +0 -0
  89. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/scope_config.py +0 -0
  90. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/stack_protector_evidence.py +0 -0
  91. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/third_party_filter.py +0 -0
  92. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/uploader.py +0 -0
  93. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/weak_prng_context.py +0 -0
  94. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/web/__init__.py +0 -0
  95. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/web/nuclei_binary.py +0 -0
  96. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/web/scan_blocklist.py +0 -0
  97. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/web/ssrf_guard.py +0 -0
  98. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy_cli.egg-info/SOURCES.txt +0 -0
  99. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy_cli.egg-info/dependency_links.txt +0 -0
  100. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy_cli.egg-info/entry_points.txt +0 -0
  101. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy_cli.egg-info/requires.txt +0 -0
  102. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy_cli.egg-info/top_level.txt +0 -0
  103. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/pyproject.toml +0 -0
  104. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/setup.cfg +0 -0
  105. {narvy_cli-1.0.1 → narvy_cli-1.0.3}/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.3
4
4
  Summary: Free, local SAST scanner for Android, iOS, and polyglot web/backend source - plus passive web, host-audit, and cloud-posture scanning.
5
5
  Author: Narvy
6
6
  License: Apache-2.0
@@ -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.3"
@@ -35,6 +35,102 @@ _SKIP_DIR_NAMES = frozenset({
35
35
  _NAMESPACE_RE = re.compile(r'\bnamespace\s*=?\s*["\']([\w.]+)["\']')
36
36
  _APPLICATION_ID_RE = re.compile(r'\bapplicationId\s*=?\s*["\']([\w.]+)["\']')
37
37
 
38
+ # Name-based hardcoded-secret rules prone to preference-key / type-name echoes.
39
+ _SECRET_NAME_RULE_IDS = frozenset({
40
+ "AND-S-001", "AND-S-002", "AND-S-010",
41
+ "android-hardcoded-credential-java", "android-hardcoded-credential-kotlin",
42
+ })
43
+ _TYPE_DECL_RE = re.compile(r'\b(?:class|interface|object|enum)\b\s+[A-Za-z_]')
44
+ _QUOTED_RE = re.compile(r'"([^"\n]*)"')
45
+ _IDENT_VALUE_RE = re.compile(r'^[a-z][a-z0-9_]*$')
46
+ _LHS_NAME_RE = re.compile(r'([A-Za-z_][A-Za-z0-9_]*)\s*(?::[^=\n]*)?=\s*"')
47
+ # Preference-key constant naming (KEY_.../PREF_...): its value is a settings-store
48
+ # key string, not a secret.
49
+ _PREFKEY_CONST_RE = re.compile(r'^(?:KEY|PREF|PREFS|PREFERENCE|SETTING|SETTINGS)_', re.IGNORECASE)
50
+
51
+ # Test / fixture locations where a headline CRITICAL/HIGH is usually a fixture.
52
+ _TEST_PATH_FRAGMENTS = (
53
+ "/test/", "/tests/", "/androidtest/", "/__tests__/", "/spec/", "/specs/",
54
+ "/fixtures/", "/fixture/", "/testdata/", "/mocks/", "/__mocks__/",
55
+ )
56
+ _TEST_FILE_RE = re.compile(
57
+ r"(?:^|[._-])(?:test|tests|spec|specs|mock|mocks|fixture|fixtures)(?:[._-]|$)",
58
+ re.IGNORECASE,
59
+ )
60
+
61
+
62
+ def _line_text(source_dir: str, rel_path: str, line: int) -> str:
63
+ try:
64
+ full = os.path.join(source_dir, rel_path)
65
+ with open(full, "r", encoding="utf-8", errors="ignore") as fh:
66
+ for i, text in enumerate(fh, start=1):
67
+ if i == line:
68
+ return text
69
+ except OSError:
70
+ pass
71
+ return ""
72
+
73
+
74
+ def _is_prefkey_echo_or_typename(line: str) -> bool:
75
+ """A preference-key-name literal echoing its constant, or a class/type name - not a real secret."""
76
+ if '"' not in line and _TYPE_DECL_RE.search(line):
77
+ return True
78
+ m = _QUOTED_RE.search(line)
79
+ if not m:
80
+ return False
81
+ val = m.group(1)
82
+ if not _IDENT_VALUE_RE.match(val):
83
+ return False
84
+ lhs = _LHS_NAME_RE.search(line)
85
+ if not lhs:
86
+ return False
87
+ name = lhs.group(1)
88
+ # Echo: the value repeats the constant name, or the constant is a KEY_*/PREF_*
89
+ # preference-key holding a settings-store key slug (e.g. KEY_SECRET_TOKEN =
90
+ # "secret_token_pref").
91
+ return val.lower() in name.lower() or bool(_PREFKEY_CONST_RE.match(name))
92
+
93
+
94
+ def _has_no_string_literal_rhs(line: str) -> bool:
95
+ """A name-based secret match whose right-hand side has NO string literal - a
96
+ function call or identifier (e.g. `val apiKey = getApiKeyOrDefault()`), not a
97
+ hardcoded secret. A real hardcoded secret is always a quoted literal."""
98
+ return not _QUOTED_RE.search(line)
99
+
100
+
101
+ def _drop_prefkey_echo_secrets(source_dir: str, findings: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
102
+ kept = []
103
+ for f in findings:
104
+ if f.get("rule_id") in _SECRET_NAME_RULE_IDS:
105
+ line = _line_text(source_dir, f.get("file_path", ""), f.get("line") or 0)
106
+ if line and (_has_no_string_literal_rhs(line) or _is_prefkey_echo_or_typename(line)):
107
+ continue
108
+ kept.append(f)
109
+ return kept
110
+
111
+
112
+ def _is_test_path(rel_path: str) -> bool:
113
+ p = "/" + rel_path.replace("\\", "/").lstrip("/")
114
+ if any(frag in p.lower() for frag in _TEST_PATH_FRAGMENTS):
115
+ return True
116
+ return bool(_TEST_FILE_RE.search(p.rsplit("/", 1)[-1]))
117
+
118
+
119
+ def _downrank_test_findings(findings: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
120
+ """Lower (never drop) CRITICAL/HIGH/MEDIUM findings in test/fixture paths to LOW and tag them."""
121
+ note = (
122
+ " [Narvy: this hit is in a test/fixture path, where such code is usually a "
123
+ "fixture rather than shipped app code, so severity is lowered - confirm it "
124
+ "is not a real production issue.]"
125
+ )
126
+ for f in findings:
127
+ if f.get("severity") in ("CRITICAL", "HIGH", "MEDIUM") and _is_test_path(f.get("file_path", "")):
128
+ f["severity"] = "LOW"
129
+ details = f.setdefault("details", {})
130
+ if isinstance(details, dict) and note.strip() not in (details.get("description") or ""):
131
+ details["description"] = (details.get("description") or "") + note
132
+ return findings
133
+
38
134
  # A module may use either source-root name, or both.
39
135
  _SRC_LEAF_DIRS = ("java", "kotlin")
40
136
  _MAX_INFER_DEPTH = 12
@@ -254,6 +350,9 @@ def analyze_source(source_dir: str, override_config: Optional[ScopeConfig] = Non
254
350
 
255
351
  rule_defs = list(rules) + semgrep_rule_defs
256
352
 
353
+ all_findings = _drop_prefkey_echo_secrets(source_dir, all_findings)
354
+ all_findings = _downrank_test_findings(all_findings)
355
+
257
356
  return {
258
357
  "ok": True,
259
358
  "error": None,
@@ -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