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.
- narvy/__init__.py +3 -0
- narvy/android/__init__.py +0 -0
- narvy/android/source_analyzer.py +268 -0
- narvy/android/split_bundle.py +475 -0
- narvy/android_rule_context.py +196 -0
- narvy/apk_memory_preflight.py +248 -0
- narvy/auth.py +40 -0
- narvy/ci_templates/bitbucket.yml +25 -0
- narvy/ci_templates/github.yml +60 -0
- narvy/ci_templates/gitlab.yml +32 -0
- narvy/cloud/__init__.py +0 -0
- narvy/cloud/aws_scan.py +1188 -0
- narvy/comment_filter.py +134 -0
- narvy/crypto_taint_lite.py +82 -0
- narvy/decompiler.py +337 -0
- narvy/doctor.py +244 -0
- narvy/host/__init__.py +0 -0
- narvy/host/audit.py +157 -0
- narvy/host/host_knowledge.py +982 -0
- narvy/host/lynis_bootstrap.py +400 -0
- narvy/host/lynis_parser.py +358 -0
- narvy/host/narvy_checks.py +789 -0
- narvy/host/report.py +69 -0
- narvy/host/ssh_exec.py +219 -0
- narvy/ios/__init__.py +0 -0
- narvy/ios/binary_analyzer.py +1201 -0
- narvy/ios/plist_checks.py +249 -0
- narvy/ios/source_analyzer.py +301 -0
- narvy/ios/third_party_filter.py +242 -0
- narvy/ios/trust_all_context.py +66 -0
- narvy/main.py +1983 -0
- narvy/native_hardening.py +503 -0
- narvy/reporter.py +151 -0
- narvy/rule_engine.py +57 -0
- narvy/rules/android/config.yml +77 -0
- narvy/rules/android/crypto.yml +34 -0
- narvy/rules/android/secrets.yml +161 -0
- narvy/rules/android/storage.yml +24 -0
- narvy/rules/android/webview.yml +23 -0
- narvy/rules/android.yml +219 -0
- narvy/rules/ios/objc/crypto.yml +56 -0
- narvy/rules/ios/objc/network.yml +67 -0
- narvy/rules/ios/objc/secrets.yml +79 -0
- narvy/rules/ios/objc/storage.yml +45 -0
- narvy/rules/ios/objc/webview.yml +45 -0
- narvy/rules/ios_swift.yml +327 -0
- narvy/rules/web/go.yml +2837 -0
- narvy/rules/web/java.yml +1576 -0
- narvy/rules/web/javascript.yml +3683 -0
- narvy/rules/web/kotlin.yml +413 -0
- narvy/rules/web/local/csharp_narvy/config.yml +61 -0
- narvy/rules/web/local/csharp_narvy/crypto.yml +64 -0
- narvy/rules/web/local/csharp_narvy/deserialization.yml +59 -0
- narvy/rules/web/local/csharp_narvy/injection.yml +122 -0
- narvy/rules/web/local/csharp_narvy/xxe.yml +48 -0
- narvy/rules/web/local/java_narvy/auth_jwt.yml +134 -0
- narvy/rules/web/local/java_narvy/deserialization.yml +108 -0
- narvy/rules/web/local/java_narvy/mybatis.yml +39 -0
- narvy/rules/web/local/java_narvy/snakeyaml.yml +34 -0
- narvy/rules/web/local/java_narvy/spring_authz.yml +32 -0
- narvy/rules/web/local/java_narvy/spring_config.yml +67 -0
- narvy/rules/web/local/java_narvy/spring_hardening.yml +291 -0
- narvy/rules/web/local/java_narvy/sqli.yml +235 -0
- narvy/rules/web/local/java_narvy/xxe.yml +212 -0
- narvy/rules/web/php.yml +1644 -0
- narvy/rules/web/python.yml +3967 -0
- narvy/rules/web/ruby.yml +703 -0
- narvy/rules/web/rust.yml +258 -0
- narvy/rules/web/secrets.yml +1420 -0
- narvy/rules/web/secrets_supplement.yml +383 -0
- narvy/sca/__init__.py +1 -0
- narvy/sca/android_deps.py +349 -0
- narvy/sca/ios_deps.py +578 -0
- narvy/sca/osv_client.py +617 -0
- narvy/sca/web_deps.py +955 -0
- narvy/scope_config.py +195 -0
- narvy/semgrep_engine.py +219 -0
- narvy/stack_protector_evidence.py +96 -0
- narvy/third_party_filter.py +211 -0
- narvy/uploader.py +92 -0
- narvy/weak_prng_context.py +270 -0
- narvy/web/__init__.py +0 -0
- narvy/web/nuclei_binary.py +105 -0
- narvy/web/scan_blocklist.py +96 -0
- narvy/web/scanner.py +842 -0
- narvy/web/source_analyzer.py +714 -0
- narvy/web/ssrf_guard.py +374 -0
- narvy_cli-1.0.0.dist-info/METADATA +165 -0
- narvy_cli-1.0.0.dist-info/RECORD +93 -0
- narvy_cli-1.0.0.dist-info/WHEEL +5 -0
- narvy_cli-1.0.0.dist-info/entry_points.txt +2 -0
- narvy_cli-1.0.0.dist-info/licenses/LICENSE +202 -0
- narvy_cli-1.0.0.dist-info/top_level.txt +1 -0
narvy/web/ssrf_guard.py
ADDED
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
"""SSRF defense for the web scanner: URL validation, IP pinning, and
|
|
2
|
+
redirect-aware safe fetching.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import ipaddress
|
|
6
|
+
import os
|
|
7
|
+
import socket
|
|
8
|
+
from typing import List, Optional, Set, Tuple
|
|
9
|
+
from urllib.parse import urlparse, urljoin
|
|
10
|
+
|
|
11
|
+
import requests
|
|
12
|
+
from requests.adapters import HTTPAdapter
|
|
13
|
+
from urllib3.poolmanager import PoolManager
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _allowlisted_hosts() -> Set[str]:
|
|
17
|
+
"""Opt-in allowlist of host[:port] entries that bypass the IP blocklist."""
|
|
18
|
+
raw = os.environ.get('SSRF_GUARD_ALLOW_HOSTS', '')
|
|
19
|
+
return {h.strip().lower() for h in raw.split(',') if h.strip()}
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
from loguru import logger
|
|
23
|
+
except Exception: # pragma: no cover - loguru always present in prod
|
|
24
|
+
import logging
|
|
25
|
+
logger = logging.getLogger("ssrf_guard")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# Blocked by name too: split-horizon DNS can point these at a benign IP.
|
|
29
|
+
METADATA_HOSTNAMES = {
|
|
30
|
+
'localhost',
|
|
31
|
+
'metadata.google.internal',
|
|
32
|
+
'metadata',
|
|
33
|
+
'instance-data',
|
|
34
|
+
'instance-data.ec2.internal',
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
# IPv4 nets the stdlib does not flag but a scanner must never reach.
|
|
38
|
+
_BLOCKED_V4_NETS = (
|
|
39
|
+
ipaddress.ip_network('100.64.0.0/10'), # CGNAT / carrier-grade NAT range
|
|
40
|
+
ipaddress.ip_network('0.0.0.0/8'), # "this network"
|
|
41
|
+
ipaddress.ip_network('198.18.0.0/15'), # benchmarking
|
|
42
|
+
ipaddress.ip_network('192.0.0.0/24'), # IETF protocol assignments
|
|
43
|
+
ipaddress.ip_network('240.0.0.0/4'), # future-use / reserved
|
|
44
|
+
ipaddress.ip_network('224.0.0.0/4'), # multicast
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# Mostly covered by the stdlib is_* properties; listed explicitly for clarity.
|
|
48
|
+
_BLOCKED_V6_NETS = (
|
|
49
|
+
ipaddress.ip_network('fc00::/7'), # unique-local (ULA)
|
|
50
|
+
ipaddress.ip_network('fe80::/10'), # link-local (also is_link_local)
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
MAX_URL_LEN = 2048
|
|
54
|
+
DEFAULT_MAX_REDIRECTS = 5
|
|
55
|
+
|
|
56
|
+
# Caps buffered body so a hostile target cannot OOM the process.
|
|
57
|
+
MAX_RESPONSE_BYTES = 25 * 1024 * 1024
|
|
58
|
+
_READ_CHUNK = 65536
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class SSRFBlocked(Exception):
|
|
62
|
+
"""Raised when a URL is rejected by the SSRF guard. The message is safe to show."""
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class ResponseTooLarge(Exception):
|
|
66
|
+
"""Raised when a target's response exceeds MAX_RESPONSE_BYTES."""
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _ip_is_blocked(ip_obj) -> bool:
|
|
70
|
+
# IPv4-mapped IPv6 (e.g. ::ffff:127.0.0.1) is unwrapped and re-checked.
|
|
71
|
+
if isinstance(ip_obj, ipaddress.IPv6Address) and ip_obj.ipv4_mapped:
|
|
72
|
+
ip_obj = ip_obj.ipv4_mapped
|
|
73
|
+
|
|
74
|
+
if (
|
|
75
|
+
ip_obj.is_private
|
|
76
|
+
or ip_obj.is_loopback
|
|
77
|
+
or ip_obj.is_link_local
|
|
78
|
+
or ip_obj.is_reserved
|
|
79
|
+
or ip_obj.is_multicast
|
|
80
|
+
or ip_obj.is_unspecified
|
|
81
|
+
):
|
|
82
|
+
return True
|
|
83
|
+
|
|
84
|
+
if isinstance(ip_obj, ipaddress.IPv4Address):
|
|
85
|
+
# Cloud metadata range; already covered by is_link_local, kept explicit.
|
|
86
|
+
if str(ip_obj).startswith('169.254.'):
|
|
87
|
+
return True
|
|
88
|
+
if str(ip_obj) == '255.255.255.255':
|
|
89
|
+
return True
|
|
90
|
+
for net in _BLOCKED_V4_NETS:
|
|
91
|
+
if ip_obj in net:
|
|
92
|
+
return True
|
|
93
|
+
else:
|
|
94
|
+
for net in _BLOCKED_V6_NETS:
|
|
95
|
+
if ip_obj in net:
|
|
96
|
+
return True
|
|
97
|
+
|
|
98
|
+
return False
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _resolve_all_ips(hostname: str, port: int) -> List[Tuple[int, str]]:
|
|
102
|
+
results: List[Tuple[int, str]] = []
|
|
103
|
+
try:
|
|
104
|
+
infos = socket.getaddrinfo(hostname, port, family=socket.AF_UNSPEC,
|
|
105
|
+
type=socket.SOCK_STREAM)
|
|
106
|
+
except socket.gaierror as e:
|
|
107
|
+
raise SSRFBlocked(f"Could not resolve hostname: {hostname}") from e
|
|
108
|
+
|
|
109
|
+
for family, _stype, _proto, _canon, sockaddr in infos:
|
|
110
|
+
ip_str = sockaddr[0]
|
|
111
|
+
# Strip any IPv6 scope id (e.g. fe80::1%eth0).
|
|
112
|
+
if '%' in ip_str:
|
|
113
|
+
ip_str = ip_str.split('%', 1)[0]
|
|
114
|
+
results.append((family, ip_str))
|
|
115
|
+
|
|
116
|
+
if not results:
|
|
117
|
+
raise SSRFBlocked(f"Could not resolve hostname: {hostname}")
|
|
118
|
+
return results
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def validate_url(url: str) -> Tuple[str, str, int]:
|
|
122
|
+
"""Validate a URL, returning (validated_url, pinned_ip, port) or raising."""
|
|
123
|
+
if not url or not isinstance(url, str):
|
|
124
|
+
raise SSRFBlocked("Empty or invalid URL")
|
|
125
|
+
if len(url) > MAX_URL_LEN:
|
|
126
|
+
raise SSRFBlocked(f"URL too long (max {MAX_URL_LEN} characters)")
|
|
127
|
+
|
|
128
|
+
# urlparse() and .port raise on a malformed IPv6 bracket or bad port.
|
|
129
|
+
try:
|
|
130
|
+
parsed = urlparse(url)
|
|
131
|
+
scheme = (parsed.scheme or '').lower()
|
|
132
|
+
hostname = parsed.hostname
|
|
133
|
+
port_value = parsed.port
|
|
134
|
+
except ValueError as e:
|
|
135
|
+
raise SSRFBlocked(f"Invalid URL: {e}")
|
|
136
|
+
|
|
137
|
+
if scheme not in ('http', 'https'):
|
|
138
|
+
raise SSRFBlocked("Only HTTP and HTTPS URLs are allowed")
|
|
139
|
+
|
|
140
|
+
if not hostname:
|
|
141
|
+
raise SSRFBlocked("Invalid URL - no hostname found")
|
|
142
|
+
hostname = hostname.lower()
|
|
143
|
+
|
|
144
|
+
port = port_value or (443 if scheme == 'https' else 80)
|
|
145
|
+
|
|
146
|
+
_allow = _allowlisted_hosts()
|
|
147
|
+
if _allow and (hostname in _allow or f"{hostname}:{port}" in _allow):
|
|
148
|
+
try:
|
|
149
|
+
literal = ipaddress.ip_address(hostname)
|
|
150
|
+
return url, str(literal), port
|
|
151
|
+
except ValueError:
|
|
152
|
+
# Allowlisted by name, still resolved so there is an IP to pin.
|
|
153
|
+
try:
|
|
154
|
+
resolved = _resolve_all_ips(hostname, port)
|
|
155
|
+
return url, resolved[0][1], port
|
|
156
|
+
except SSRFBlocked:
|
|
157
|
+
raise
|
|
158
|
+
|
|
159
|
+
if hostname in METADATA_HOSTNAMES:
|
|
160
|
+
raise SSRFBlocked("Scanning internal/metadata addresses is not allowed")
|
|
161
|
+
|
|
162
|
+
try:
|
|
163
|
+
literal = ipaddress.ip_address(hostname)
|
|
164
|
+
if _ip_is_blocked(literal):
|
|
165
|
+
raise SSRFBlocked("Scanning internal/private/reserved IP addresses is not allowed")
|
|
166
|
+
return url, str(literal), port
|
|
167
|
+
except ValueError:
|
|
168
|
+
pass
|
|
169
|
+
|
|
170
|
+
resolved = _resolve_all_ips(hostname, port)
|
|
171
|
+
|
|
172
|
+
pinned_ip: Optional[str] = None
|
|
173
|
+
for _family, ip_str in resolved:
|
|
174
|
+
try:
|
|
175
|
+
ip_obj = ipaddress.ip_address(ip_str)
|
|
176
|
+
except ValueError:
|
|
177
|
+
raise SSRFBlocked("Resolved to an unparseable address")
|
|
178
|
+
if _ip_is_blocked(ip_obj):
|
|
179
|
+
# Anti-rebinding: one dangerous record rejects the whole host.
|
|
180
|
+
raise SSRFBlocked(
|
|
181
|
+
"Host resolves to an internal/private/reserved address - blocked"
|
|
182
|
+
)
|
|
183
|
+
if pinned_ip is None:
|
|
184
|
+
pinned_ip = ip_str
|
|
185
|
+
|
|
186
|
+
if pinned_ip is None:
|
|
187
|
+
raise SSRFBlocked(f"Could not resolve hostname: {hostname}")
|
|
188
|
+
|
|
189
|
+
return url, pinned_ip, port
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class SSRFSafeAdapter(HTTPAdapter):
|
|
193
|
+
"""HTTPAdapter that pins the TCP connection to a pre-validated IP, keeping the
|
|
194
|
+
hostname for Host header/SNI so it cannot be re-resolved between validate and connect."""
|
|
195
|
+
|
|
196
|
+
def __init__(self, pinned_host: str, pinned_ip: str, *args, **kwargs):
|
|
197
|
+
self._pinned_host = pinned_host.lower()
|
|
198
|
+
self._pinned_ip = pinned_ip
|
|
199
|
+
super().__init__(*args, **kwargs)
|
|
200
|
+
|
|
201
|
+
def init_poolmanager(self, connections, maxsize, block=False, **pool_kwargs):
|
|
202
|
+
# Pinning happens in send() via a scoped create_connection swap.
|
|
203
|
+
self.poolmanager = PoolManager(
|
|
204
|
+
num_pools=connections, maxsize=maxsize, block=block, **pool_kwargs
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
def _make_pinned_create_connection(self, orig_create_connection):
|
|
208
|
+
pinned_host = self._pinned_host
|
|
209
|
+
pinned_ip = self._pinned_ip
|
|
210
|
+
|
|
211
|
+
def _pinned_create_connection(address, *a, **kw):
|
|
212
|
+
host = address[0] if address else None
|
|
213
|
+
if host and host.lower() == pinned_host:
|
|
214
|
+
address = (pinned_ip,) + tuple(address[1:])
|
|
215
|
+
return orig_create_connection(address, *a, **kw)
|
|
216
|
+
|
|
217
|
+
return _pinned_create_connection
|
|
218
|
+
|
|
219
|
+
def send(self, request, *args, **kwargs):
|
|
220
|
+
import urllib3.util.connection as _u3c
|
|
221
|
+
import urllib3.connection as _u3conn
|
|
222
|
+
_orig_util = _u3c.create_connection
|
|
223
|
+
pinned = self._make_pinned_create_connection(_orig_util)
|
|
224
|
+
_orig_conn = getattr(_u3conn, 'create_connection', None)
|
|
225
|
+
_u3c.create_connection = pinned
|
|
226
|
+
if _orig_conn is not None:
|
|
227
|
+
_u3conn.create_connection = pinned
|
|
228
|
+
try:
|
|
229
|
+
return super().send(request, *args, **kwargs)
|
|
230
|
+
finally:
|
|
231
|
+
_u3c.create_connection = _orig_util
|
|
232
|
+
if _orig_conn is not None:
|
|
233
|
+
_u3conn.create_connection = _orig_conn
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _build_pinned_session(
|
|
237
|
+
validated_url: str,
|
|
238
|
+
pinned_ip: str,
|
|
239
|
+
base_session: Optional[requests.Session] = None,
|
|
240
|
+
user_agent: Optional[str] = None,
|
|
241
|
+
) -> requests.Session:
|
|
242
|
+
host = urlparse(validated_url).hostname.lower()
|
|
243
|
+
if base_session is not None:
|
|
244
|
+
sess = base_session
|
|
245
|
+
else:
|
|
246
|
+
sess = requests.Session()
|
|
247
|
+
sess.verify = False
|
|
248
|
+
if user_agent:
|
|
249
|
+
sess.headers.setdefault('User-Agent', user_agent)
|
|
250
|
+
|
|
251
|
+
adapter = SSRFSafeAdapter(pinned_host=host, pinned_ip=pinned_ip)
|
|
252
|
+
sess.mount('http://', adapter)
|
|
253
|
+
sess.mount('https://', adapter)
|
|
254
|
+
return sess
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _materialize_capped(resp: requests.Response, url: str) -> None:
|
|
258
|
+
"""Buffer a streamed response body into ``resp``, capped at MAX_RESPONSE_BYTES."""
|
|
259
|
+
declared = resp.headers.get('Content-Length')
|
|
260
|
+
if declared is not None:
|
|
261
|
+
try:
|
|
262
|
+
if int(declared) > MAX_RESPONSE_BYTES:
|
|
263
|
+
resp.close()
|
|
264
|
+
raise ResponseTooLarge(
|
|
265
|
+
f"{url}: declared Content-Length {declared} exceeds the "
|
|
266
|
+
f"{MAX_RESPONSE_BYTES} byte cap")
|
|
267
|
+
except ValueError:
|
|
268
|
+
pass
|
|
269
|
+
|
|
270
|
+
buf = bytearray()
|
|
271
|
+
try:
|
|
272
|
+
for chunk in resp.iter_content(chunk_size=_READ_CHUNK):
|
|
273
|
+
if not chunk:
|
|
274
|
+
continue
|
|
275
|
+
buf += chunk
|
|
276
|
+
if len(buf) > MAX_RESPONSE_BYTES:
|
|
277
|
+
resp.close()
|
|
278
|
+
raise ResponseTooLarge(
|
|
279
|
+
f"{url}: response body exceeded the {MAX_RESPONSE_BYTES} "
|
|
280
|
+
f"byte cap (streaming)")
|
|
281
|
+
finally:
|
|
282
|
+
resp.close()
|
|
283
|
+
|
|
284
|
+
# The two attributes `requests` sets on a cached body, so .content/.text/.json() work.
|
|
285
|
+
resp._content = bytes(buf)
|
|
286
|
+
resp._content_consumed = True
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def safe_get(
|
|
290
|
+
url: str,
|
|
291
|
+
*,
|
|
292
|
+
timeout: int = 10,
|
|
293
|
+
headers: Optional[dict] = None,
|
|
294
|
+
session: Optional[requests.Session] = None,
|
|
295
|
+
max_redirects: int = DEFAULT_MAX_REDIRECTS,
|
|
296
|
+
verify: bool = False,
|
|
297
|
+
user_agent: Optional[str] = None,
|
|
298
|
+
**kwargs,
|
|
299
|
+
) -> requests.Response:
|
|
300
|
+
"""SSRF-safe GET: every redirect hop is re-validated and re-pinned. Cookies and
|
|
301
|
+
headers from ``session`` are preserved across hops."""
|
|
302
|
+
kwargs.pop('allow_redirects', None)
|
|
303
|
+
|
|
304
|
+
current_url = url
|
|
305
|
+
# Base session carries cookies; each hop gets a fresh pinned adapter (host/IP may differ).
|
|
306
|
+
base = session if session is not None else None
|
|
307
|
+
if base is None:
|
|
308
|
+
base = requests.Session()
|
|
309
|
+
base.verify = verify
|
|
310
|
+
if user_agent:
|
|
311
|
+
base.headers.setdefault('User-Agent', user_agent)
|
|
312
|
+
|
|
313
|
+
last_resp: Optional[requests.Response] = None
|
|
314
|
+
for hop in range(max_redirects + 1):
|
|
315
|
+
validated_url, pinned_ip, _port = validate_url(current_url)
|
|
316
|
+
sess = _build_pinned_session(validated_url, pinned_ip, base_session=base)
|
|
317
|
+
|
|
318
|
+
resp = sess.get(
|
|
319
|
+
validated_url,
|
|
320
|
+
timeout=timeout,
|
|
321
|
+
headers=headers,
|
|
322
|
+
allow_redirects=False,
|
|
323
|
+
verify=verify,
|
|
324
|
+
stream=True,
|
|
325
|
+
**kwargs,
|
|
326
|
+
)
|
|
327
|
+
last_resp = resp
|
|
328
|
+
|
|
329
|
+
if resp.is_redirect or resp.status_code in (301, 302, 303, 307, 308):
|
|
330
|
+
location = resp.headers.get('Location')
|
|
331
|
+
if not location:
|
|
332
|
+
_materialize_capped(resp, validated_url)
|
|
333
|
+
return resp
|
|
334
|
+
# A hop's body is closed unread, so it cannot smuggle a huge body past the cap.
|
|
335
|
+
next_url = urljoin(validated_url, location)
|
|
336
|
+
logger.debug(f"[ssrf_guard] redirect hop {hop}: {validated_url} -> {next_url}")
|
|
337
|
+
current_url = next_url
|
|
338
|
+
resp.close()
|
|
339
|
+
continue
|
|
340
|
+
|
|
341
|
+
_materialize_capped(resp, validated_url)
|
|
342
|
+
return resp
|
|
343
|
+
|
|
344
|
+
logger.warning(f"[ssrf_guard] max redirects ({max_redirects}) exceeded for {url}")
|
|
345
|
+
if last_resp is not None:
|
|
346
|
+
_materialize_capped(last_resp, current_url)
|
|
347
|
+
return last_resp
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def safe_session(
|
|
351
|
+
url: str,
|
|
352
|
+
*,
|
|
353
|
+
base_session: Optional[requests.Session] = None,
|
|
354
|
+
user_agent: Optional[str] = None,
|
|
355
|
+
verify: bool = False,
|
|
356
|
+
) -> Tuple[requests.Session, str]:
|
|
357
|
+
"""Return (session, validated_url) pinned to ``url``'s IP, for one request.
|
|
358
|
+
Callers that follow redirects must use safe_get, which re-validates each hop."""
|
|
359
|
+
validated_url, pinned_ip, _port = validate_url(url)
|
|
360
|
+
sess = _build_pinned_session(
|
|
361
|
+
validated_url, pinned_ip, base_session=base_session, user_agent=user_agent
|
|
362
|
+
)
|
|
363
|
+
sess.verify = verify
|
|
364
|
+
return sess, validated_url
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def is_safe_url(url: str) -> bool:
|
|
368
|
+
try:
|
|
369
|
+
validate_url(url)
|
|
370
|
+
return True
|
|
371
|
+
except SSRFBlocked:
|
|
372
|
+
return False
|
|
373
|
+
except Exception:
|
|
374
|
+
return False
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: narvy-cli
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Free, local SAST scanner for Android, iOS, and polyglot web/backend source - plus passive web, host-audit, and cloud-posture scanning.
|
|
5
|
+
Author: Narvy
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://narvy.io
|
|
8
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
9
|
+
Classifier: Environment :: Console
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Security
|
|
18
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: pyaxmlparser
|
|
23
|
+
Requires-Dist: pyyaml
|
|
24
|
+
Requires-Dist: rich
|
|
25
|
+
Requires-Dist: requests
|
|
26
|
+
Requires-Dist: semgrep==1.152.0
|
|
27
|
+
Requires-Dist: lief
|
|
28
|
+
Provides-Extra: ios-full
|
|
29
|
+
Requires-Dist: icdump; extra == "ios-full"
|
|
30
|
+
Provides-Extra: cloud
|
|
31
|
+
Requires-Dist: boto3; extra == "cloud"
|
|
32
|
+
Dynamic: license-file
|
|
33
|
+
|
|
34
|
+
# Narvy CLI
|
|
35
|
+
|
|
36
|
+
[](https://badge.fury.io/py/narvy-cli)
|
|
37
|
+
[](https://opensource.org/licenses/Apache-2.0)
|
|
38
|
+
|
|
39
|
+
A free, local static application security testing (SAST) scanner. It runs
|
|
40
|
+
entirely on your machine, needs no account, and finds real, high-signal
|
|
41
|
+
vulnerabilities - hardcoded secrets, insecure configuration, weak
|
|
42
|
+
cryptography, SSRF, injection, and more - in:
|
|
43
|
+
|
|
44
|
+
- **Android**: `.apk` / `.aab`, split-APK bundles (`.apkm` / `.xapk` /
|
|
45
|
+
`.apks`), or a raw Gradle/Kotlin source checkout
|
|
46
|
+
- **iOS**: an unencrypted `.ipa`, or a raw Xcode/Swift source checkout
|
|
47
|
+
- **Web/backend source**: JavaScript/TypeScript, Python, PHP, Go, Ruby,
|
|
48
|
+
Rust, Java/Kotlin, C#/.NET
|
|
49
|
+
|
|
50
|
+
It also ships three standalone scanners that need nothing but network
|
|
51
|
+
access or your own existing credentials:
|
|
52
|
+
|
|
53
|
+
- `web-scan` - passive Nuclei-based scan of a live URL
|
|
54
|
+
- `host-audit` - agentless host audit over your own SSH access (Narvy's own
|
|
55
|
+
host checks plus Lynis (GPL-3.0) as the hardening baseline)
|
|
56
|
+
- `cloud-scan` - AWS account posture check using your own ambient AWS
|
|
57
|
+
credentials
|
|
58
|
+
|
|
59
|
+
Nothing is uploaded anywhere unless you explicitly pass `--upload`.
|
|
60
|
+
|
|
61
|
+
## Install
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
pip install narvy-cli
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Requires a Java 11+ JRE on your `PATH` for Android scanning (jadx itself is
|
|
68
|
+
auto-downloaded on first scan into `~/.narvy/tools`). Everything else
|
|
69
|
+
is pure Python plus a couple of optional extras:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
pip install 'narvy-cli[ios-full]' # full Obj-C introspection for iOS binary scans
|
|
73
|
+
pip install 'narvy-cli[cloud]' # boto3, needed for cloud-scan
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Usage
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
# Android APK/AAB, split bundle, or a Gradle/Kotlin source checkout
|
|
80
|
+
narvy scan app-release.apk
|
|
81
|
+
narvy scan app.apkm
|
|
82
|
+
narvy scan ./my-android-app/
|
|
83
|
+
|
|
84
|
+
# iOS: unencrypted IPA, or a Swift/Xcode source checkout
|
|
85
|
+
narvy scan MyApp.ipa
|
|
86
|
+
narvy scan ./my-ios-app/
|
|
87
|
+
|
|
88
|
+
# Web/backend source (Node, Python, PHP, Go, Ruby, Rust, Java/Kotlin, C#/.NET)
|
|
89
|
+
narvy scan ./my-api-repo/
|
|
90
|
+
|
|
91
|
+
# SARIF output, for CI or GitHub/GitLab code scanning
|
|
92
|
+
narvy scan app-release.apk --output sarif --file results.sarif
|
|
93
|
+
|
|
94
|
+
# Passive web scan of a live URL - no active injection payloads, ever
|
|
95
|
+
narvy web-scan https://example.com
|
|
96
|
+
|
|
97
|
+
# Agentless host hardening audit over your own SSH access
|
|
98
|
+
narvy host-audit example.com --user deploy
|
|
99
|
+
|
|
100
|
+
# AWS account posture check using your own aws configure / env vars / IAM role
|
|
101
|
+
narvy cloud-scan
|
|
102
|
+
|
|
103
|
+
# List recent scans on your Narvy account (requires `narvy login`)
|
|
104
|
+
narvy scans
|
|
105
|
+
|
|
106
|
+
# Scan + upload every .apk/.aab/.apkm/.xapk/.ipa found in a folder, in one command
|
|
107
|
+
narvy upload-all ./my-apps-folder/
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Run `narvy doctor` before your first scan to check your Java/jadx/iOS
|
|
111
|
+
tooling setup, and `narvy <command> --help` for the full flag list of
|
|
112
|
+
any subcommand.
|
|
113
|
+
|
|
114
|
+
### Very large Android apps
|
|
115
|
+
|
|
116
|
+
Decompiling is memory-hungry, and file size is a poor predictor of how much
|
|
117
|
+
you need. Before decompiling, the CLI reads the DEX headers to estimate the
|
|
118
|
+
Java heap the app needs and checks it against `--max-mem` and your
|
|
119
|
+
machine's free memory, so an app that won't fit fails in about a second
|
|
120
|
+
instead of after several minutes of doomed work. The estimate is a
|
|
121
|
+
heuristic, not a hard limit - `--force` runs the decompile regardless.
|
|
122
|
+
|
|
123
|
+
### Optional Narvy Cloud scan
|
|
124
|
+
|
|
125
|
+
`narvy login` links the CLI to a Narvy account. Add `--upload`
|
|
126
|
+
to a scan to also submit it for a deeper cloud pass (full taint
|
|
127
|
+
analysis, compliance mapping, dashboard history) - not required for local
|
|
128
|
+
scanning, and not on by default. `narvy upload-all <directory>` does
|
|
129
|
+
the same for every binary target found in a folder in one command.
|
|
130
|
+
`narvy scans` lists recent scans on your account from the terminal.
|
|
131
|
+
|
|
132
|
+
## Known limitations
|
|
133
|
+
|
|
134
|
+
- Encrypted iOS binaries (the normal App Store distribution state) are
|
|
135
|
+
detected but not decrypted; only the local, static checks that don't need
|
|
136
|
+
the decrypted binary run.
|
|
137
|
+
- Web-source coverage depth isn't uniform across all eight languages - Rust
|
|
138
|
+
and C#/.NET have a smaller rule set than the others, and the CLI says so
|
|
139
|
+
in its own output when it applies.
|
|
140
|
+
- Only one architecture's native libraries are checked in a multi-ABI
|
|
141
|
+
APK/split bundle.
|
|
142
|
+
|
|
143
|
+
See [SETUP.md](SETUP.md) for detailed install/troubleshooting steps and the
|
|
144
|
+
exact commands this README's examples were run against.
|
|
145
|
+
|
|
146
|
+
## License
|
|
147
|
+
|
|
148
|
+
Apache License 2.0 - see [LICENSE](LICENSE).
|
|
149
|
+
|
|
150
|
+
Rule packs under `narvy/rules/` are authored by Narvy
|
|
151
|
+
(Apache 2.0, same as the rest of this package) and contain no third-party
|
|
152
|
+
rule text.
|
|
153
|
+
`host-audit` runs Narvy's own host checks (secrets exposed on disk,
|
|
154
|
+
unauthenticated data services, cloud metadata exposure, container/daemon
|
|
155
|
+
posture, exposed VCS/artifacts in web roots) and, as the hardening baseline,
|
|
156
|
+
shells out to [Lynis](https://cisofy.com/lynis/) (GPL-3.0), auto-fetched at
|
|
157
|
+
run time over your SSH access, run arm's-length, never bundled. Lynis is
|
|
158
|
+
attributed as the engine in the audit output; its controls are not presented
|
|
159
|
+
as Narvy's own.
|
|
160
|
+
|
|
161
|
+
## Contributing
|
|
162
|
+
|
|
163
|
+
Issues and pull requests welcome, especially new or improved detection
|
|
164
|
+
rules. Rule packs live under `narvy/rules/`: Android/iOS
|
|
165
|
+
(`rules/android`, `rules/ios`) and web/backend (`rules/web`, Semgrep-based).
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
narvy/__init__.py,sha256=IQxw1-Fgen70bKkXPsnhdnJKTnWRXaE_AwP5DbpyNvY,96
|
|
2
|
+
narvy/android_rule_context.py,sha256=6jzMyT4D5NpLcys32PLxqHClNKqvb7sCNCVA3N51P7U,7811
|
|
3
|
+
narvy/apk_memory_preflight.py,sha256=AIQ81rW2C0hXAqhYbTDrEH3lKj7fSF6ZXAA_iQl3_8A,10278
|
|
4
|
+
narvy/auth.py,sha256=to9_Luc6TgyQnZa5RcHELHQeaSTiY-Y2coX6WVe7Cm8,1160
|
|
5
|
+
narvy/comment_filter.py,sha256=absOixsgQrvMI1BzradWZQ6iS90HxWsIhE5P1tNjj3s,3918
|
|
6
|
+
narvy/crypto_taint_lite.py,sha256=aac8sM_o91gxVjEPlaQOe6GK0aSqM358Fh58BKKP7yg,3337
|
|
7
|
+
narvy/decompiler.py,sha256=cwXyikAkjNe5NNcymqRGFJe45XIiirP3PJfkqK95ljY,13198
|
|
8
|
+
narvy/doctor.py,sha256=IZ6wDs2R95BYKyY8kxgKyfFnFQ0hX6OAJqXD7sq2TtE,9869
|
|
9
|
+
narvy/main.py,sha256=6yJnOk6x35qxQSblRo88SDcFjHRAth5NmXo0_HJC-sw,97720
|
|
10
|
+
narvy/native_hardening.py,sha256=sQaSEFgnhHQ9ES2vWE3XDH74O5HHvu5qZSWBfykgvLw,20507
|
|
11
|
+
narvy/reporter.py,sha256=MV9CE6Otb9sGmUQxFWcIGjCWRtvh6I2tCvlwdevFwEs,5739
|
|
12
|
+
narvy/rule_engine.py,sha256=aKc0K_Xj0bGtXVaQAQI0m2E8gNl2xYHNAXfp1OEZX1U,2365
|
|
13
|
+
narvy/scope_config.py,sha256=Y5rHbNSh81TKNuFTxGlW-pX6Ov4xsBFsmW5JBFPHzJo,6862
|
|
14
|
+
narvy/semgrep_engine.py,sha256=PpWXSwcSZmZa6eEkGhd4ViKAbM8DZI5evm7Oc7dFirc,8564
|
|
15
|
+
narvy/stack_protector_evidence.py,sha256=9MgTkAqp1twnN-qJFBgYa4xg3WR5C1RSjARg-nF1DLc,3740
|
|
16
|
+
narvy/third_party_filter.py,sha256=Nvb5Mdx5FTGvCb2BfYoTxHXf6fOlwJtyvjMn1XsK6j8,7987
|
|
17
|
+
narvy/uploader.py,sha256=L7x3cn5p9Lu7VXqlZRtjJnKxiO_wLQHCmL-rFU_CuNo,2860
|
|
18
|
+
narvy/weak_prng_context.py,sha256=eYLiNw2p4ua8rbms3QhK5f8bifPyVUQ77Aq4XHm-woc,10080
|
|
19
|
+
narvy/android/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
20
|
+
narvy/android/source_analyzer.py,sha256=0roHyMfVViZDcmFI-73aVRYoYa9LOwEyErjzXkqSxXQ,10092
|
|
21
|
+
narvy/android/split_bundle.py,sha256=L4Y15OUHxeePX1AwlZQOb6cNeZhtn6Fkci0--qGlQVU,18881
|
|
22
|
+
narvy/ci_templates/bitbucket.yml,sha256=uYCvi1QH4p4U3B9XMGljf9rRCrGOUdTEcNr9mWHmPiY,978
|
|
23
|
+
narvy/ci_templates/github.yml,sha256=cWgnxeeXEJhkLtPgTbbDJZ30LGmQ4X3rlLY63SU-qyU,2002
|
|
24
|
+
narvy/ci_templates/gitlab.yml,sha256=BWBNu_EsAZRVK1lkbEoReUvqwRLsYbrXFETNGGS18VI,1234
|
|
25
|
+
narvy/cloud/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
26
|
+
narvy/cloud/aws_scan.py,sha256=DgUEwB6pZGf7LMX8P0Lv8CNMGNmeglfsZijms3tADiQ,53110
|
|
27
|
+
narvy/host/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
28
|
+
narvy/host/audit.py,sha256=6-wsjqDaOpJx9RZMJh_O7THWYnYnMUhaf7gqGE9TABs,5453
|
|
29
|
+
narvy/host/host_knowledge.py,sha256=pyw6iy7jSdRWow508IrPKmiRM3A744JG6lanLvZl0L0,52391
|
|
30
|
+
narvy/host/lynis_bootstrap.py,sha256=G2AqcPklBwScbNhCGBC7hUoOdXMtf3CD2yrwZxDes7E,15594
|
|
31
|
+
narvy/host/lynis_parser.py,sha256=jEK5UwyAqXnQL-cKvErZ-ho0NWBwL2V840HgzZNfCO0,12669
|
|
32
|
+
narvy/host/narvy_checks.py,sha256=TGuErYdgBsNgPY9XAfKaTK6GMBrJnU1A2vbROmbm55s,31179
|
|
33
|
+
narvy/host/report.py,sha256=hhSPpBrE4SP1xukg3ExyQ7Op6N_NAb_qwIBLZ0RT6vg,2397
|
|
34
|
+
narvy/host/ssh_exec.py,sha256=nh7xjMQcnJvk-i9mk8m-9s31Wjzp4CsHRQhnL4-aqLY,7517
|
|
35
|
+
narvy/ios/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
36
|
+
narvy/ios/binary_analyzer.py,sha256=NnPjHXC_7GJ4IHFdlCYH6BVvGALlLaY_bhrlKRsOULQ,56326
|
|
37
|
+
narvy/ios/plist_checks.py,sha256=XghxO8Y1YsbWpoEqf8M_p4i5mD-ZERep15PEaqx8Hpw,11693
|
|
38
|
+
narvy/ios/source_analyzer.py,sha256=xCIKzXOICkanqdQlHdibdRXqTWXWbJZhrgoRrKiP4Os,11086
|
|
39
|
+
narvy/ios/third_party_filter.py,sha256=wl-bLev2nfuTd30Ga0qU8tm-COEB4oftWk1y20ZXIEI,8955
|
|
40
|
+
narvy/ios/trust_all_context.py,sha256=H_iVaEmJecLAGIpPEGZnDkot9BmMuQVDcXPNWunEdcQ,1993
|
|
41
|
+
narvy/rules/android.yml,sha256=MmG_-jTUy5nOZDkTerWFLjfIu0GQANQiZeJz9H0lqGM,8172
|
|
42
|
+
narvy/rules/ios_swift.yml,sha256=RSPQPcETrS9KMUxaHFrS2Bh3d430N491RI1u3JrijxA,12318
|
|
43
|
+
narvy/rules/android/config.yml,sha256=qUtCazhmV02FZd81qoi9ZYq8dH2uAGGPk70Z_AfU4X0,4909
|
|
44
|
+
narvy/rules/android/crypto.yml,sha256=wyf4fuDJG5m4_klqTJw7zTPhTo1sDHBqsXi_d_P5W3Q,1744
|
|
45
|
+
narvy/rules/android/secrets.yml,sha256=pK1pTlc_nQcQvjF4NgvGtlL46Qo_psKOaBZNdRphBI4,8555
|
|
46
|
+
narvy/rules/android/storage.yml,sha256=-0rkA-XtIlnCuPxJpG7NsI9IkDDRGrnq23Y5LFrXD80,1475
|
|
47
|
+
narvy/rules/android/webview.yml,sha256=kWIGPuQ6UVLbgGyvj-3MsaIf8NrLn-NG4JZW4sYh7Cw,1049
|
|
48
|
+
narvy/rules/ios/objc/crypto.yml,sha256=yPPqcDCbM8Gc8jGdFowrudZPGXT0vnvV5DrtpJmMn1Y,2874
|
|
49
|
+
narvy/rules/ios/objc/network.yml,sha256=2ryYViC43FFuUm9O4CtFojcrNWbM6fU5CM9nDfW_kCA,4295
|
|
50
|
+
narvy/rules/ios/objc/secrets.yml,sha256=8NFYEN3uzsj3vFKgbl4hDBalLVn2I0nGwfeLWqashxQ,4356
|
|
51
|
+
narvy/rules/ios/objc/storage.yml,sha256=asXrK7YeJZTa76uDpYv7LgS3rEl7PRRcuf7SKuSwolA,2788
|
|
52
|
+
narvy/rules/ios/objc/webview.yml,sha256=uusi2CQLrQrcrKmDJ1e1q51lkmG5aXUNH8GwrLAQRl4,2985
|
|
53
|
+
narvy/rules/web/go.yml,sha256=NhjOvlmUllanfguDAyJH9P-n0vYl7a0wXFMFOhWzpLk,100033
|
|
54
|
+
narvy/rules/web/java.yml,sha256=BzzA02D7nyccF2dUv0HSw3Ap53EmQtWcV4s7G9RLtLk,55805
|
|
55
|
+
narvy/rules/web/javascript.yml,sha256=wx7ZAF9a2yn5Ymu8k_lp-SSVA25MygIP47o1YHhg_48,147582
|
|
56
|
+
narvy/rules/web/kotlin.yml,sha256=7oKcqfJ4Gfcz7yte6MobH8aUnoebkVs0e2ihoxYB5Xk,14360
|
|
57
|
+
narvy/rules/web/php.yml,sha256=PE3w9HXW3o8HPKOQW9RsubqMY9_u2bBMpAlDuXQP85c,54459
|
|
58
|
+
narvy/rules/web/python.yml,sha256=O4-YjldmNgtFrb-KUBoce_fycXyOAq3qoj9NYAqD34s,140549
|
|
59
|
+
narvy/rules/web/ruby.yml,sha256=DMeiAR7ZdWG34lKQ9PS2gmZv0AIK1NrwhYpv_A-29kg,25949
|
|
60
|
+
narvy/rules/web/rust.yml,sha256=oY0srMOsGM7D3dr8ciN0H2lzy6ii2AGo-xaizsm9Z8A,9265
|
|
61
|
+
narvy/rules/web/secrets.yml,sha256=8YX4fUKWW4RZ5oau-Iog8JhMteomDmkN3IDYj5tx7tM,41287
|
|
62
|
+
narvy/rules/web/secrets_supplement.yml,sha256=-Qda5MljM189L3DZ8DXPWbZZnmzDUyjVOSEXgCnMFVc,9924
|
|
63
|
+
narvy/rules/web/local/csharp_narvy/config.yml,sha256=pTAxBVLzid8-7aYk2biZZfffBUbQP-qS1fkWkx4esWs,2082
|
|
64
|
+
narvy/rules/web/local/csharp_narvy/crypto.yml,sha256=oRBhPjI7wc8r2ba9RBOdUQsB_EXBtPNbWeqlNRH6PAM,2313
|
|
65
|
+
narvy/rules/web/local/csharp_narvy/deserialization.yml,sha256=PfegDUx7ysFpv644ihytaPmhP4PpHNPGS3f7UcjkekQ,2254
|
|
66
|
+
narvy/rules/web/local/csharp_narvy/injection.yml,sha256=7tiI5hvF4nxobPXXvKQCoApTpX6SN6S-_vzfbt5OWZU,5140
|
|
67
|
+
narvy/rules/web/local/csharp_narvy/xxe.yml,sha256=2y1Kycs2FWPWf0zVZy_49XeMLnUQgwuptL5_8P6ifPM,1633
|
|
68
|
+
narvy/rules/web/local/java_narvy/auth_jwt.yml,sha256=wFN81YbT7bAYjv42k6hSKkms9RzZ1iZPG7WKOoZoKFg,5046
|
|
69
|
+
narvy/rules/web/local/java_narvy/deserialization.yml,sha256=wiMTGKpqWup0HzGhpIWE1EEoUr8GCqo4iN5lMr_vdm4,4415
|
|
70
|
+
narvy/rules/web/local/java_narvy/mybatis.yml,sha256=LcBiPVL7wSKGyjtrNDpzHyt0UEjp7Aw5_ZImj2aPcHk,1697
|
|
71
|
+
narvy/rules/web/local/java_narvy/snakeyaml.yml,sha256=_DkLcyHyOaZTrozriqFRIPAt6KyaMdlkGMcrOizJJyA,1269
|
|
72
|
+
narvy/rules/web/local/java_narvy/spring_authz.yml,sha256=RrdtjgijRpr_VFs0sscbWpnAvgD91Tlmd_yFEC7pI2U,1137
|
|
73
|
+
narvy/rules/web/local/java_narvy/spring_config.yml,sha256=5f3UZRueTfQF1Q9-eBaJXc9NZcOZLCKXge6KW30eU1g,2405
|
|
74
|
+
narvy/rules/web/local/java_narvy/spring_hardening.yml,sha256=REZKg5t92eq32riTnvaijPpFukZ_5G6gK8usZAEVGJY,10940
|
|
75
|
+
narvy/rules/web/local/java_narvy/sqli.yml,sha256=fMJXogX9XJG-R79Re2OChNcrd0Jf4p22ybDtnLAbFpU,8652
|
|
76
|
+
narvy/rules/web/local/java_narvy/xxe.yml,sha256=immbF7DrTS3rYzeBpwlO2pjuTLyq9DMVrs_mhhCt7Cg,7798
|
|
77
|
+
narvy/sca/__init__.py,sha256=yf6o8Yj140CoYXLNxx8Sg7cxwS0Cfd-WOGi2ueVyqT8,74
|
|
78
|
+
narvy/sca/android_deps.py,sha256=zx1fNZkDuMZ-Zl6UZSzQtezfQ2oHbs82wlBGQPwXVHE,13109
|
|
79
|
+
narvy/sca/ios_deps.py,sha256=72MNCxx1SiEnxrGgfommFe8M33CbWg2aeqtV7co0sNo,21560
|
|
80
|
+
narvy/sca/osv_client.py,sha256=cP3nC5x2IGXqu2FMdjkzveg0WXV8XvXWTsbhdtBLriw,20532
|
|
81
|
+
narvy/sca/web_deps.py,sha256=L2xqvCDs5MWzrcdbEsF-l7P3zWgdKgQJWfG-toMBrFU,34409
|
|
82
|
+
narvy/web/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
83
|
+
narvy/web/nuclei_binary.py,sha256=S5zzPSEYiYDjS830INHOFD44WFa0k2Th4IMfsvMYIm0,3570
|
|
84
|
+
narvy/web/scan_blocklist.py,sha256=toKPe4Z7OH2TAfTaTTpplZrzaRiP8LIRtvq-wx-hNtM,3250
|
|
85
|
+
narvy/web/scanner.py,sha256=liJRqtSKMPJ-75_C9Kpzls7TuC6nzxdzQdoFvmahd6g,34687
|
|
86
|
+
narvy/web/source_analyzer.py,sha256=CNzb2E0B1Ive-wIFOl7cDpGyTfxoBTLMbAxzN8zpyxs,27313
|
|
87
|
+
narvy/web/ssrf_guard.py,sha256=Fem_GbuTSQT9P88KFksbf1ru7O0kQqC_buFVFU_E45M,12806
|
|
88
|
+
narvy_cli-1.0.0.dist-info/licenses/LICENSE,sha256=oRS_yX70S6CuKY_6fQvPIaql-fucDYMZG9Mo_I9VPww,11341
|
|
89
|
+
narvy_cli-1.0.0.dist-info/METADATA,sha256=1VwaPJdcGq0bOobxfNusqTnp8CwzV0Ee0SkBBzWUcgc,6390
|
|
90
|
+
narvy_cli-1.0.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
91
|
+
narvy_cli-1.0.0.dist-info/entry_points.txt,sha256=B9il_Cpsbt7F1Gk6ePswqmu7oduBalCtbJheDFSbQu8,41
|
|
92
|
+
narvy_cli-1.0.0.dist-info/top_level.txt,sha256=z_E-mw3b3pmziLN825vSN-d7FqP5-QC4Cel_nRy-Jm8,6
|
|
93
|
+
narvy_cli-1.0.0.dist-info/RECORD,,
|