narvy-cli 1.0.0__py3-none-any.whl

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 (93) hide show
  1. narvy/__init__.py +3 -0
  2. narvy/android/__init__.py +0 -0
  3. narvy/android/source_analyzer.py +268 -0
  4. narvy/android/split_bundle.py +475 -0
  5. narvy/android_rule_context.py +196 -0
  6. narvy/apk_memory_preflight.py +248 -0
  7. narvy/auth.py +40 -0
  8. narvy/ci_templates/bitbucket.yml +25 -0
  9. narvy/ci_templates/github.yml +60 -0
  10. narvy/ci_templates/gitlab.yml +32 -0
  11. narvy/cloud/__init__.py +0 -0
  12. narvy/cloud/aws_scan.py +1188 -0
  13. narvy/comment_filter.py +134 -0
  14. narvy/crypto_taint_lite.py +82 -0
  15. narvy/decompiler.py +337 -0
  16. narvy/doctor.py +244 -0
  17. narvy/host/__init__.py +0 -0
  18. narvy/host/audit.py +157 -0
  19. narvy/host/host_knowledge.py +982 -0
  20. narvy/host/lynis_bootstrap.py +400 -0
  21. narvy/host/lynis_parser.py +358 -0
  22. narvy/host/narvy_checks.py +789 -0
  23. narvy/host/report.py +69 -0
  24. narvy/host/ssh_exec.py +219 -0
  25. narvy/ios/__init__.py +0 -0
  26. narvy/ios/binary_analyzer.py +1201 -0
  27. narvy/ios/plist_checks.py +249 -0
  28. narvy/ios/source_analyzer.py +301 -0
  29. narvy/ios/third_party_filter.py +242 -0
  30. narvy/ios/trust_all_context.py +66 -0
  31. narvy/main.py +1983 -0
  32. narvy/native_hardening.py +503 -0
  33. narvy/reporter.py +151 -0
  34. narvy/rule_engine.py +57 -0
  35. narvy/rules/android/config.yml +77 -0
  36. narvy/rules/android/crypto.yml +34 -0
  37. narvy/rules/android/secrets.yml +161 -0
  38. narvy/rules/android/storage.yml +24 -0
  39. narvy/rules/android/webview.yml +23 -0
  40. narvy/rules/android.yml +219 -0
  41. narvy/rules/ios/objc/crypto.yml +56 -0
  42. narvy/rules/ios/objc/network.yml +67 -0
  43. narvy/rules/ios/objc/secrets.yml +79 -0
  44. narvy/rules/ios/objc/storage.yml +45 -0
  45. narvy/rules/ios/objc/webview.yml +45 -0
  46. narvy/rules/ios_swift.yml +327 -0
  47. narvy/rules/web/go.yml +2837 -0
  48. narvy/rules/web/java.yml +1576 -0
  49. narvy/rules/web/javascript.yml +3683 -0
  50. narvy/rules/web/kotlin.yml +413 -0
  51. narvy/rules/web/local/csharp_narvy/config.yml +61 -0
  52. narvy/rules/web/local/csharp_narvy/crypto.yml +64 -0
  53. narvy/rules/web/local/csharp_narvy/deserialization.yml +59 -0
  54. narvy/rules/web/local/csharp_narvy/injection.yml +122 -0
  55. narvy/rules/web/local/csharp_narvy/xxe.yml +48 -0
  56. narvy/rules/web/local/java_narvy/auth_jwt.yml +134 -0
  57. narvy/rules/web/local/java_narvy/deserialization.yml +108 -0
  58. narvy/rules/web/local/java_narvy/mybatis.yml +39 -0
  59. narvy/rules/web/local/java_narvy/snakeyaml.yml +34 -0
  60. narvy/rules/web/local/java_narvy/spring_authz.yml +32 -0
  61. narvy/rules/web/local/java_narvy/spring_config.yml +67 -0
  62. narvy/rules/web/local/java_narvy/spring_hardening.yml +291 -0
  63. narvy/rules/web/local/java_narvy/sqli.yml +235 -0
  64. narvy/rules/web/local/java_narvy/xxe.yml +212 -0
  65. narvy/rules/web/php.yml +1644 -0
  66. narvy/rules/web/python.yml +3967 -0
  67. narvy/rules/web/ruby.yml +703 -0
  68. narvy/rules/web/rust.yml +258 -0
  69. narvy/rules/web/secrets.yml +1420 -0
  70. narvy/rules/web/secrets_supplement.yml +383 -0
  71. narvy/sca/__init__.py +1 -0
  72. narvy/sca/android_deps.py +349 -0
  73. narvy/sca/ios_deps.py +578 -0
  74. narvy/sca/osv_client.py +617 -0
  75. narvy/sca/web_deps.py +955 -0
  76. narvy/scope_config.py +195 -0
  77. narvy/semgrep_engine.py +219 -0
  78. narvy/stack_protector_evidence.py +96 -0
  79. narvy/third_party_filter.py +211 -0
  80. narvy/uploader.py +92 -0
  81. narvy/weak_prng_context.py +270 -0
  82. narvy/web/__init__.py +0 -0
  83. narvy/web/nuclei_binary.py +105 -0
  84. narvy/web/scan_blocklist.py +96 -0
  85. narvy/web/scanner.py +842 -0
  86. narvy/web/source_analyzer.py +714 -0
  87. narvy/web/ssrf_guard.py +374 -0
  88. narvy_cli-1.0.0.dist-info/METADATA +165 -0
  89. narvy_cli-1.0.0.dist-info/RECORD +93 -0
  90. narvy_cli-1.0.0.dist-info/WHEEL +5 -0
  91. narvy_cli-1.0.0.dist-info/entry_points.txt +2 -0
  92. narvy_cli-1.0.0.dist-info/licenses/LICENSE +202 -0
  93. narvy_cli-1.0.0.dist-info/top_level.txt +1 -0
narvy/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """Narvy CLI - free, local SAST for Android, iOS and polyglot source."""
2
+
3
+ __version__ = "1.0.0"
File without changes
@@ -0,0 +1,268 @@
1
+ """Android source-repo (Gradle/Java/Kotlin) SAST entry point.
2
+
3
+ Public API: analyze_source(source_dir, override_config=None) -> dict.
4
+ """
5
+ from __future__ import annotations
6
+
7
+ import os
8
+ import re
9
+ from typing import Any, Dict, List, Optional, Tuple
10
+
11
+ from narvy.rule_engine import load_rules_from_dir, run_rules_on_file
12
+ from narvy import semgrep_engine
13
+ from narvy import crypto_taint_lite
14
+ from narvy.scope_config import ScopeConfig
15
+ from narvy.third_party_filter import (
16
+ GRADLE_SOURCE_MARKERS,
17
+ extract_component_package_roots,
18
+ parse_manifest_package_attr,
19
+ resolve_file_scope,
20
+ )
21
+
22
+ _HERE = os.path.dirname(__file__)
23
+ ANDROID_RULES_DIR = os.path.join(_HERE, "..", "rules", "android")
24
+
25
+ _MANIFEST_FILENAME = "AndroidManifest.xml"
26
+ _GRADLE_BUILD_FILENAMES = ("build.gradle.kts", "build.gradle")
27
+ _SRC_CODE_EXTS = (".java", ".kt")
28
+
29
+ # Build output, tool caches and VCS dirs, pruned from every walk here.
30
+ _SKIP_DIR_NAMES = frozenset({
31
+ "build", ".gradle", ".idea", ".git", "out", ".cxx", "node_modules",
32
+ ".vscode", ".vs", "captures",
33
+ })
34
+
35
+ _NAMESPACE_RE = re.compile(r'\bnamespace\s*=?\s*["\']([\w.]+)["\']')
36
+ _APPLICATION_ID_RE = re.compile(r'\bapplicationId\s*=?\s*["\']([\w.]+)["\']')
37
+
38
+ # A module may use either source-root name, or both.
39
+ _SRC_LEAF_DIRS = ("java", "kotlin")
40
+ _MAX_INFER_DEPTH = 12
41
+
42
+
43
+ def _should_prune_dir(name: str) -> bool:
44
+ return name in _SKIP_DIR_NAMES or name.startswith(".")
45
+
46
+
47
+ def find_android_manifests(source_dir: str) -> List[str]:
48
+ manifests = []
49
+ for root, dirs, files in os.walk(source_dir):
50
+ dirs[:] = [d for d in dirs if not _should_prune_dir(d)]
51
+ if _MANIFEST_FILENAME in files:
52
+ manifests.append(os.path.join(root, _MANIFEST_FILENAME))
53
+ return manifests
54
+
55
+
56
+ def _module_dir_for_manifest(manifest_path: str) -> Optional[str]:
57
+ """The module dir for a `<module>/src/<variant>/AndroidManifest.xml` path."""
58
+ main_dir = os.path.dirname(manifest_path)
59
+ src_dir = os.path.dirname(main_dir)
60
+ if os.path.basename(src_dir) == "src":
61
+ return os.path.dirname(src_dir)
62
+ return None
63
+
64
+
65
+ def _read_gradle_namespace_and_app_id(module_dir: str) -> Tuple[Optional[str], Optional[str]]:
66
+ """Read `namespace` and `applicationId` from a module's build.gradle(.kts) (regex; computed values yield None)."""
67
+ namespace, app_id = None, None
68
+ for fname in _GRADLE_BUILD_FILENAMES:
69
+ fpath = os.path.join(module_dir, fname)
70
+ if not os.path.isfile(fpath):
71
+ continue
72
+ try:
73
+ with open(fpath, "r", encoding="utf-8", errors="ignore") as f:
74
+ content = f.read()
75
+ except OSError:
76
+ continue
77
+ if namespace is None:
78
+ m = _NAMESPACE_RE.search(content)
79
+ if m:
80
+ namespace = m.group(1)
81
+ if app_id is None:
82
+ m = _APPLICATION_ID_RE.search(content)
83
+ if m:
84
+ app_id = m.group(1)
85
+ return namespace, app_id
86
+
87
+
88
+ def _infer_root_packages_from_dir(main_dir: str) -> set:
89
+ """Infer a module's top package from its source dir chain of single-child directories."""
90
+ roots = set()
91
+ for leaf in _SRC_LEAF_DIRS:
92
+ src_root = os.path.join(main_dir, leaf)
93
+ if not os.path.isdir(src_root):
94
+ continue
95
+ segments: List[str] = []
96
+ current = src_root
97
+ for _ in range(_MAX_INFER_DEPTH):
98
+ try:
99
+ entries = os.listdir(current)
100
+ except OSError:
101
+ break
102
+ subdirs = [e for e in entries if not e.startswith(".")
103
+ and os.path.isdir(os.path.join(current, e))]
104
+ has_files = any(not e.startswith(".") and os.path.isfile(os.path.join(current, e))
105
+ for e in entries)
106
+ if has_files or len(subdirs) != 1:
107
+ break
108
+ segments.append(subdirs[0])
109
+ current = os.path.join(current, subdirs[0])
110
+ if segments:
111
+ roots.add(".".join(segments))
112
+ return roots
113
+
114
+
115
+ def get_own_package_roots_gradle(manifest_paths: List[str]) -> set:
116
+ all_roots = set()
117
+ for manifest_path in manifest_paths:
118
+ main_dir = os.path.dirname(manifest_path)
119
+ try:
120
+ with open(manifest_path, "r", encoding="utf-8", errors="ignore") as f:
121
+ content = f.read()
122
+ except OSError:
123
+ content = ""
124
+
125
+ module_dir = _module_dir_for_manifest(manifest_path)
126
+ namespace, app_id = (None, None)
127
+ if module_dir:
128
+ namespace, app_id = _read_gradle_namespace_and_app_id(module_dir)
129
+ manifest_pkg = parse_manifest_package_attr(content)
130
+ dir_roots = _infer_root_packages_from_dir(main_dir)
131
+
132
+ # Anchor for a component's relative android:name=".Foo".
133
+ anchor = namespace or manifest_pkg or (sorted(dir_roots)[0] if dir_roots else None)
134
+
135
+ module_roots = set(dir_roots)
136
+ for candidate in (namespace, manifest_pkg, app_id):
137
+ if candidate:
138
+ module_roots.add(candidate)
139
+ module_roots |= extract_component_package_roots(content, anchor)
140
+
141
+ all_roots |= module_roots
142
+ return all_roots
143
+
144
+
145
+ def _collect_source_files(source_dir: str) -> Tuple[List[str], List[str]]:
146
+ code_files: List[str] = []
147
+ xml_files: List[str] = []
148
+ for root, dirs, files in os.walk(source_dir):
149
+ dirs[:] = [d for d in dirs if not _should_prune_dir(d)]
150
+ for fname in files:
151
+ if fname.endswith(_SRC_CODE_EXTS):
152
+ code_files.append(os.path.join(root, fname))
153
+ elif fname.endswith(".xml"):
154
+ xml_files.append(os.path.join(root, fname))
155
+ return code_files, xml_files
156
+
157
+
158
+ # The '*' matches any source-set variant, not just 'main'.
159
+ _SEMGREP_INCLUDE_PREFIXES = ["src/*/java/", "src/*/kotlin/"]
160
+
161
+
162
+ def analyze_source(source_dir: str, override_config: Optional[ScopeConfig] = None) -> Dict[str, Any]:
163
+ """Run Android source-mode SAST against a Gradle/Java/Kotlin checkout. Read-only."""
164
+ empty_stats = {
165
+ "manifests_found": 0, "own_roots": [], "code_files_scanned": 0,
166
+ "xml_files_scanned": 0, "skipped_third_party": 0,
167
+ }
168
+ if not source_dir or not os.path.isdir(source_dir):
169
+ return {
170
+ "ok": False,
171
+ "error": f"Not a directory: {source_dir}",
172
+ "findings": [],
173
+ "rule_defs": [],
174
+ "notes": [],
175
+ **empty_stats,
176
+ }
177
+
178
+ notes: List[str] = []
179
+ all_findings: List[Dict[str, Any]] = []
180
+
181
+ manifest_paths = sorted(find_android_manifests(source_dir))
182
+ own_roots = get_own_package_roots_gradle(manifest_paths)
183
+
184
+ if not manifest_paths:
185
+ notes.append(
186
+ "No AndroidManifest.xml found anywhere in this tree - this looked "
187
+ "like an Android Gradle project (an Android Gradle plugin id / "
188
+ "`android { }` block / AndroidX flag was found) but no module's "
189
+ "own package could be confirmed. "
190
+ "Falling back to the known-SDK denylist instead of an own-package "
191
+ "allowlist, which is less precise (may include some third-party "
192
+ "code, should not miss any of your own)."
193
+ )
194
+ elif not own_roots:
195
+ notes.append(
196
+ f"Found {len(manifest_paths)} AndroidManifest.xml file(s) but "
197
+ "could not determine any module's own package (no `namespace` in "
198
+ "build.gradle, no manifest package= attribute, no source files "
199
+ "under src/main/java or src/main/kotlin). Falling back to the "
200
+ "known-SDK denylist instead of an own-package allowlist."
201
+ )
202
+
203
+ code_files, xml_files = _collect_source_files(source_dir)
204
+ all_source_files = code_files + xml_files
205
+
206
+ source_files = [
207
+ f for f in all_source_files
208
+ if resolve_file_scope(f, own_roots, override_config, markers=GRADLE_SOURCE_MARKERS)
209
+ ]
210
+ skipped_third_party = len(all_source_files) - len(source_files)
211
+
212
+ rules = load_rules_from_dir(ANDROID_RULES_DIR)
213
+ code_files_scanned = 0
214
+ xml_files_scanned = 0
215
+ for file_path in source_files:
216
+ rel_path = os.path.relpath(file_path, source_dir)
217
+ findings_in_file = run_rules_on_file(file_path, rules)
218
+ for finding in findings_in_file:
219
+ finding["file_path"] = rel_path
220
+ finding["engine"] = "regex"
221
+ all_findings.extend(findings_in_file)
222
+
223
+ if file_path.endswith(_SRC_CODE_EXTS):
224
+ code_files_scanned += 1
225
+ try:
226
+ with open(file_path, "r", encoding="utf-8", errors="ignore") as fh:
227
+ taint_findings = crypto_taint_lite.check_file(fh.read())
228
+ except OSError:
229
+ taint_findings = []
230
+ for finding in taint_findings:
231
+ finding["file_path"] = rel_path
232
+ all_findings.extend(taint_findings)
233
+ else:
234
+ xml_files_scanned += 1
235
+
236
+ semgrep_rule_defs: List[Dict[str, Any]] = []
237
+ if semgrep_engine.is_available():
238
+ semgrep_findings = semgrep_engine.run_semgrep(
239
+ source_dir, own_roots=own_roots, override_config=override_config,
240
+ include_path_prefixes=_SEMGREP_INCLUDE_PREFIXES,
241
+ )
242
+ if semgrep_findings:
243
+ all_findings.extend(semgrep_findings)
244
+ # A timed-out semgrep pass returns [], which must not read as clean.
245
+ _sg_note = semgrep_engine.degraded_coverage_note()
246
+ if _sg_note:
247
+ notes.append(_sg_note)
248
+ semgrep_rule_defs = semgrep_engine.load_rule_defs()
249
+ else:
250
+ notes.append(
251
+ "semgrep not found - install with `pip install semgrep` for "
252
+ "deeper structural detection. Regex rules still ran."
253
+ )
254
+
255
+ rule_defs = list(rules) + semgrep_rule_defs
256
+
257
+ return {
258
+ "ok": True,
259
+ "error": None,
260
+ "findings": all_findings,
261
+ "rule_defs": rule_defs,
262
+ "manifests_found": len(manifest_paths),
263
+ "own_roots": sorted(own_roots),
264
+ "code_files_scanned": code_files_scanned,
265
+ "xml_files_scanned": xml_files_scanned,
266
+ "skipped_third_party": skipped_third_party,
267
+ "notes": notes,
268
+ }