narvy-cli 1.1.0__tar.gz → 1.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/CHANGELOG.md +28 -0
- {narvy_cli-1.1.0/narvy_cli.egg-info → narvy_cli-1.1.1}/PKG-INFO +5 -2
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/README.md +4 -1
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/narvy/__init__.py +1 -1
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/narvy/main.py +91 -15
- narvy_cli-1.1.1/narvy/own_reports.py +72 -0
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/narvy/rules/web/python.yml +67 -0
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/narvy/uploader.py +6 -0
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/narvy/web/source_analyzer.py +5 -2
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/narvy/web/ssrf_guard.py +2 -0
- {narvy_cli-1.1.0 → narvy_cli-1.1.1/narvy_cli.egg-info}/PKG-INFO +5 -2
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/narvy_cli.egg-info/SOURCES.txt +1 -0
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/LICENSE +0 -0
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/MANIFEST.in +0 -0
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/SECURITY.md +0 -0
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/narvy/android/__init__.py +0 -0
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/narvy/android/source_analyzer.py +0 -0
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/narvy/android/split_bundle.py +0 -0
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/narvy/android_rule_context.py +0 -0
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/narvy/apk_memory_preflight.py +0 -0
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/narvy/auth.py +0 -0
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/narvy/ci_templates/bitbucket.yml +0 -0
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/narvy/ci_templates/github.yml +0 -0
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/narvy/ci_templates/gitlab.yml +0 -0
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/narvy/cloud/__init__.py +0 -0
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/narvy/cloud/aws_scan.py +0 -0
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/narvy/comment_filter.py +0 -0
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/narvy/crypto_taint_lite.py +0 -0
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/narvy/decompiler.py +0 -0
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/narvy/doctor.py +0 -0
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/narvy/host/__init__.py +0 -0
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/narvy/host/audit.py +0 -0
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/narvy/host/host_knowledge.py +0 -0
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/narvy/host/lynis_bootstrap.py +0 -0
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/narvy/host/lynis_parser.py +0 -0
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/narvy/host/narvy_checks.py +0 -0
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/narvy/host/report.py +0 -0
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/narvy/host/ssh_exec.py +0 -0
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/narvy/ios/__init__.py +0 -0
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/narvy/ios/binary_analyzer.py +0 -0
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/narvy/ios/plist_checks.py +0 -0
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/narvy/ios/source_analyzer.py +0 -0
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/narvy/ios/third_party_filter.py +0 -0
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/narvy/ios/trust_all_context.py +0 -0
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/narvy/native_hardening.py +0 -0
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/narvy/proc.py +0 -0
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/narvy/reporter.py +0 -0
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/narvy/rule_engine.py +0 -0
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/narvy/rules/android/config.yml +0 -0
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/narvy/rules/android/crypto.yml +0 -0
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/narvy/rules/android/secrets.yml +0 -0
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/narvy/rules/android/storage.yml +0 -0
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/narvy/rules/android/webview.yml +0 -0
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/narvy/rules/android.yml +0 -0
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/narvy/rules/ios/objc/crypto.yml +0 -0
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/narvy/rules/ios/objc/network.yml +0 -0
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/narvy/rules/ios/objc/secrets.yml +0 -0
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/narvy/rules/ios/objc/storage.yml +0 -0
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/narvy/rules/ios/objc/webview.yml +0 -0
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/narvy/rules/ios_swift.yml +0 -0
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/narvy/rules/web/go.yml +0 -0
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/narvy/rules/web/java.yml +0 -0
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/narvy/rules/web/javascript.yml +0 -0
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/narvy/rules/web/kotlin.yml +0 -0
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/narvy/rules/web/php.yml +0 -0
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/narvy/rules/web/ruby.yml +0 -0
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/narvy/rules/web/rust.yml +0 -0
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/narvy/rules/web/secrets.yml +0 -0
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/narvy/rules/web/secrets_supplement.yml +0 -0
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/narvy/sca/__init__.py +0 -0
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/narvy/sca/android_deps.py +0 -0
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/narvy/sca/ios_deps.py +0 -0
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/narvy/sca/osv_client.py +0 -0
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/narvy/sca/web_deps.py +0 -0
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/narvy/scope_config.py +0 -0
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/narvy/semgrep_engine.py +0 -0
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/narvy/stack_protector_evidence.py +0 -0
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/narvy/telemetry.py +0 -0
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/narvy/third_party_filter.py +0 -0
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/narvy/weak_prng_context.py +0 -0
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/narvy/web/__init__.py +0 -0
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/narvy/web/nuclei_binary.py +0 -0
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/narvy/web/scan_blocklist.py +0 -0
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/narvy/web/scanner.py +0 -0
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/narvy_cli.egg-info/dependency_links.txt +0 -0
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/narvy_cli.egg-info/entry_points.txt +0 -0
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/narvy_cli.egg-info/requires.txt +0 -0
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/narvy_cli.egg-info/top_level.txt +0 -0
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/pyproject.toml +0 -0
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/setup.cfg +0 -0
- {narvy_cli-1.1.0 → narvy_cli-1.1.1}/setup.py +0 -0
|
@@ -5,6 +5,34 @@ All notable changes to this project are documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.1.1]
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Flask cookie checks: `set_cookie()` without `httponly=True` or
|
|
13
|
+
`secure=True`, the same checks Django already had.
|
|
14
|
+
- `scan` warns when files or folders could not be read (permissions) instead
|
|
15
|
+
of skipping them silently; `-v` lists them and the JSON report has
|
|
16
|
+
`summary.unreadable_paths`.
|
|
17
|
+
- `scan --format` is accepted as an alias of `--output`, like the other
|
|
18
|
+
commands.
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- A second `scan` of the same folder picked up the report written by the
|
|
23
|
+
first one (`narvy.sarif`, the JSON report) and flagged the rule text quoted
|
|
24
|
+
in it as secrets. Narvy reports are now recognized by their content, and
|
|
25
|
+
the `--file` target is never scanned.
|
|
26
|
+
- `--upload` with only `NARVY_TOKEN` set printed "Uploading ... to for
|
|
27
|
+
server-side curation". The line now names the server (`narvy.io`, or
|
|
28
|
+
`NARVY_URL`), and a failed upload or login says which server refused it.
|
|
29
|
+
- `web-scan http://localhost` said "internal/metadata addresses"; it now says
|
|
30
|
+
the address is loopback and explains why local and private targets are
|
|
31
|
+
refused.
|
|
32
|
+
- The web-scan, host-audit and cloud-scan JSON output carries a `tool` field.
|
|
33
|
+
- `narvy doctor` documents its exit codes (0 all required checks pass, 1 one
|
|
34
|
+
failed).
|
|
35
|
+
|
|
8
36
|
## [1.1.0]
|
|
9
37
|
|
|
10
38
|
First release of the community rule set. The 1.0.x preview releases were
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: narvy-cli
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.1
|
|
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
|
|
@@ -143,6 +143,9 @@ command. `narvy scans` lists recent scans on your account from the terminal.
|
|
|
143
143
|
| 4 | The scan, login, upload or report write failed |
|
|
144
144
|
| 130 | Interrupted (Ctrl-C, SIGTERM) |
|
|
145
145
|
|
|
146
|
+
`narvy doctor` exits 0 when every required check passes and 1 when one fails.
|
|
147
|
+
Missing optional tools are shown as INFO and do not change its exit code.
|
|
148
|
+
|
|
146
149
|
### Resource limits
|
|
147
150
|
|
|
148
151
|
The structural analysis pass runs with at most 4 parallel workers and a
|
|
@@ -165,7 +168,7 @@ Exactly these fields are sent:
|
|
|
165
168
|
|-------|---------|
|
|
166
169
|
| `schema` | `1`, the event format version |
|
|
167
170
|
| `install_id` | a random UUID created on first run and stored in `~/.narvy/config.json` (not derived from your machine, user or account) |
|
|
168
|
-
| `cli_version`, `python_version`, `os` | `1.1.
|
|
171
|
+
| `cli_version`, `python_version`, `os` | `1.1.1`, `3.11`, `linux` / `darwin` / `windows` / `other` |
|
|
169
172
|
| `ci` | `true` when a CI environment variable is set |
|
|
170
173
|
| `command` | `scan`, `web-scan`, `host-audit`, ... |
|
|
171
174
|
| `surface` | `android`, `ios`, `source`, `web`, `host`, `cloud` or `none` |
|
|
@@ -110,6 +110,9 @@ command. `narvy scans` lists recent scans on your account from the terminal.
|
|
|
110
110
|
| 4 | The scan, login, upload or report write failed |
|
|
111
111
|
| 130 | Interrupted (Ctrl-C, SIGTERM) |
|
|
112
112
|
|
|
113
|
+
`narvy doctor` exits 0 when every required check passes and 1 when one fails.
|
|
114
|
+
Missing optional tools are shown as INFO and do not change its exit code.
|
|
115
|
+
|
|
113
116
|
### Resource limits
|
|
114
117
|
|
|
115
118
|
The structural analysis pass runs with at most 4 parallel workers and a
|
|
@@ -132,7 +135,7 @@ Exactly these fields are sent:
|
|
|
132
135
|
|-------|---------|
|
|
133
136
|
| `schema` | `1`, the event format version |
|
|
134
137
|
| `install_id` | a random UUID created on first run and stored in `~/.narvy/config.json` (not derived from your machine, user or account) |
|
|
135
|
-
| `cli_version`, `python_version`, `os` | `1.1.
|
|
138
|
+
| `cli_version`, `python_version`, `os` | `1.1.1`, `3.11`, `linux` / `darwin` / `windows` / `other` |
|
|
136
139
|
| `ci` | `true` when a CI environment variable is set |
|
|
137
140
|
| `command` | `scan`, `web-scan`, `host-audit`, ... |
|
|
138
141
|
| `surface` | `android`, `ios`, `source`, `web`, `host`, `cloud` or `none` |
|
|
@@ -38,7 +38,7 @@ from narvy import __version__
|
|
|
38
38
|
from narvy.reporter import generate_sarif_report, sarif_level, sarif_security_severity
|
|
39
39
|
from .decompiler import decompile_apk
|
|
40
40
|
from .rule_engine import load_rules_from_dir, run_rules_on_file
|
|
41
|
-
from . import auth, uploader, telemetry
|
|
41
|
+
from . import auth, own_reports, uploader, telemetry
|
|
42
42
|
from .third_party_filter import get_own_package_roots, resolve_file_scope, check_android_override
|
|
43
43
|
from .scope_config import load_scope_config
|
|
44
44
|
from . import semgrep_engine
|
|
@@ -775,7 +775,7 @@ def cmd_login(args):
|
|
|
775
775
|
raise SystemExit(EXIT_BAD_TARGET)
|
|
776
776
|
status, data = uploader.whoami(token)
|
|
777
777
|
if status != 200:
|
|
778
|
-
console.print(f"[bold red]Login failed:[/bold red] {data.get('error', status)}")
|
|
778
|
+
console.print(f"[bold red]Login to {uploader.server_host()} failed:[/bold red] {data.get('error', status)}")
|
|
779
779
|
raise SystemExit(EXIT_ERROR)
|
|
780
780
|
auth.save_token(token, data["email"], data["plan"])
|
|
781
781
|
console.print(f"[bold green]Logged in as {data['email']} ({data['plan']} plan).[/bold green]")
|
|
@@ -805,7 +805,7 @@ def cmd_scans(args):
|
|
|
805
805
|
raise SystemExit(EXIT_ERROR)
|
|
806
806
|
if status != 200:
|
|
807
807
|
error_msg = data.get("error") or data.get("message") or data
|
|
808
|
-
console.print(f"[bold red]Could not fetch scan history ({status}):[/bold red] {error_msg}")
|
|
808
|
+
console.print(f"[bold red]Could not fetch scan history from {uploader.server_host()} ({status}):[/bold red] {error_msg}")
|
|
809
809
|
raise SystemExit(EXIT_ERROR)
|
|
810
810
|
|
|
811
811
|
scans = data.get("scans", [])
|
|
@@ -821,7 +821,7 @@ def cmd_scans(args):
|
|
|
821
821
|
)
|
|
822
822
|
return
|
|
823
823
|
|
|
824
|
-
table = Table(title=f"Recent scans (
|
|
824
|
+
table = Table(title=f"Recent scans on {rich_escape(_server_and_account(creds))}")
|
|
825
825
|
table.add_column("Scan ID", style="dim")
|
|
826
826
|
table.add_column("App")
|
|
827
827
|
table.add_column("Platform")
|
|
@@ -1370,6 +1370,17 @@ def _ingest_scan_mode_label(scan_mode: str) -> str:
|
|
|
1370
1370
|
return "android-binary" if scan_mode == "android" else scan_mode
|
|
1371
1371
|
|
|
1372
1372
|
|
|
1373
|
+
def _server_and_account(creds):
|
|
1374
|
+
"""'narvy.io (you@example.com, starter)'; NARVY_TOKEN alone carries no email or plan."""
|
|
1375
|
+
parts = [p for p in ((creds or {}).get("email"), (creds or {}).get("plan")) if p]
|
|
1376
|
+
host = uploader.server_host()
|
|
1377
|
+
return f"{host} ({', '.join(parts)})" if parts else host
|
|
1378
|
+
|
|
1379
|
+
|
|
1380
|
+
def _upload_failed_label(status):
|
|
1381
|
+
return f"Upload to {uploader.server_host()} failed ({f'HTTP {status}' if status else 'no response'}):"
|
|
1382
|
+
|
|
1383
|
+
|
|
1373
1384
|
def _submit_results_ingest(all_findings, scan_mode, target_path):
|
|
1374
1385
|
"""POST results to /api/v1/ingest. Exits with EXIT_ERROR on any failure."""
|
|
1375
1386
|
creds = auth.load_credentials()
|
|
@@ -1382,7 +1393,7 @@ def _submit_results_ingest(all_findings, scan_mode, target_path):
|
|
|
1382
1393
|
app_name = re.sub(r"\.(zip|apk|aab|ipa|xapk|apkm|apks)$", "", app_name, flags=re.IGNORECASE) or app_name
|
|
1383
1394
|
|
|
1384
1395
|
console.print(
|
|
1385
|
-
f"[cyan]Uploading {len(all_findings)} finding(s) to {creds
|
|
1396
|
+
f"[cyan]Uploading {len(all_findings)} finding(s) to {rich_escape(_server_and_account(creds))} "
|
|
1386
1397
|
f"for server-side curation...[/cyan]"
|
|
1387
1398
|
)
|
|
1388
1399
|
try:
|
|
@@ -1391,7 +1402,7 @@ def _submit_results_ingest(all_findings, scan_mode, target_path):
|
|
|
1391
1402
|
scan_mode=_ingest_scan_mode_label(scan_mode), findings=all_findings,
|
|
1392
1403
|
)
|
|
1393
1404
|
except Exception as e:
|
|
1394
|
-
console.print(f"[bold red]Upload failed:[/bold red] {e}")
|
|
1405
|
+
console.print(f"[bold red]Upload to {uploader.server_host()} failed:[/bold red] {e}")
|
|
1395
1406
|
raise SystemExit(EXIT_ERROR)
|
|
1396
1407
|
|
|
1397
1408
|
if status not in (200, 201):
|
|
@@ -1400,7 +1411,7 @@ def _submit_results_ingest(all_findings, scan_mode, target_path):
|
|
|
1400
1411
|
console.print(f"[bold red]{asset_msg}[/bold red]")
|
|
1401
1412
|
raise SystemExit(EXIT_ERROR)
|
|
1402
1413
|
error_msg = data.get("error") or data if isinstance(data, dict) else data
|
|
1403
|
-
console.print(f"[bold red]
|
|
1414
|
+
console.print(f"[bold red]{_upload_failed_label(status)}[/bold red] {error_msg}")
|
|
1404
1415
|
if isinstance(data, dict) and data.get("message"):
|
|
1405
1416
|
console.print(f"[dim]{data['message']}[/dim]")
|
|
1406
1417
|
_print_upgrade_hint(status, data)
|
|
@@ -1497,6 +1508,9 @@ def _scan_to_json(all_findings, rules, scan_mode, target_path, encrypted, scan_m
|
|
|
1497
1508
|
premium = (scan_meta or {}).get("premium_checks_not_run")
|
|
1498
1509
|
if premium:
|
|
1499
1510
|
summary["premium_checks_not_run"] = premium
|
|
1511
|
+
unreadable = (scan_meta or {}).get("unreadable_paths")
|
|
1512
|
+
if unreadable:
|
|
1513
|
+
summary["unreadable_paths"] = unreadable
|
|
1500
1514
|
return {
|
|
1501
1515
|
"tool": "Narvy CLI",
|
|
1502
1516
|
"version": __version__,
|
|
@@ -1548,8 +1562,33 @@ def _scan_nested_ios_surfaces(target_path, ios_roots, all_findings, rules, scan_
|
|
|
1548
1562
|
return all_findings, rules, scan_meta
|
|
1549
1563
|
|
|
1550
1564
|
|
|
1565
|
+
_UNREADABLE_SKIP_DIRS = frozenset({
|
|
1566
|
+
".git", "node_modules", ".gradle", ".idea", "Pods", "Carthage", "__pycache__",
|
|
1567
|
+
".venv", "venv", "DerivedData", ".build",
|
|
1568
|
+
})
|
|
1569
|
+
|
|
1570
|
+
|
|
1571
|
+
def _unreadable_paths(source_dir):
|
|
1572
|
+
"""Files and directories under source_dir this user cannot read (vendored dirs aside)."""
|
|
1573
|
+
bad = []
|
|
1574
|
+
|
|
1575
|
+
def _onerror(err):
|
|
1576
|
+
if getattr(err, "filename", None):
|
|
1577
|
+
bad.append(os.path.relpath(err.filename, source_dir) + os.sep)
|
|
1578
|
+
|
|
1579
|
+
for root, dirs, files in os.walk(source_dir, onerror=_onerror):
|
|
1580
|
+
dirs[:] = [d for d in dirs if d not in _UNREADABLE_SKIP_DIRS]
|
|
1581
|
+
for name in files:
|
|
1582
|
+
full = os.path.join(root, name)
|
|
1583
|
+
if not os.path.islink(full) and not os.access(full, os.R_OK):
|
|
1584
|
+
bad.append(os.path.relpath(full, source_dir))
|
|
1585
|
+
return sorted(bad)
|
|
1586
|
+
|
|
1587
|
+
|
|
1551
1588
|
def cmd_scan(args):
|
|
1552
1589
|
target_path = args.apk_path
|
|
1590
|
+
own_reports.SKIPPED.clear()
|
|
1591
|
+
own_reports.set_output_path(None)
|
|
1553
1592
|
if getattr(args, "upload_binary", False):
|
|
1554
1593
|
args.upload = True
|
|
1555
1594
|
if not os.path.exists(target_path):
|
|
@@ -1593,6 +1632,7 @@ def cmd_scan(args):
|
|
|
1593
1632
|
console.print(f"[bold red]Cannot write {args.output.upper()} output to {_preflight_out}: {_werr}.[/bold red]")
|
|
1594
1633
|
console.print("[dim]Check the parent directory exists and is writable, or pick a different --file path.[/dim]")
|
|
1595
1634
|
raise SystemExit(EXIT_ERROR)
|
|
1635
|
+
own_reports.set_output_path(_preflight_out)
|
|
1596
1636
|
|
|
1597
1637
|
# Android and iOS-binary paths only: iOS source mode classifies vendored
|
|
1598
1638
|
# code by directory convention instead.
|
|
@@ -1602,6 +1642,8 @@ def cmd_scan(args):
|
|
|
1602
1642
|
f"{len(override_config.own)} 'own' pattern(s), "
|
|
1603
1643
|
f"{len(override_config.vendor)} 'vendor' pattern(s).[/dim]")
|
|
1604
1644
|
|
|
1645
|
+
unreadable = _unreadable_paths(target_path) if os.path.isdir(target_path) else []
|
|
1646
|
+
|
|
1605
1647
|
if scan_mode == "ios-source":
|
|
1606
1648
|
result = _run_ios_source_scan(target_path)
|
|
1607
1649
|
elif scan_mode == "ios-binary":
|
|
@@ -1637,6 +1679,16 @@ def cmd_scan(args):
|
|
|
1637
1679
|
all_findings, rules, scan_meta = _scan_nested_ios_surfaces(
|
|
1638
1680
|
target_path, extra_ios_roots, all_findings, rules, scan_meta)
|
|
1639
1681
|
|
|
1682
|
+
# Our own earlier reports (narvy.sarif, results JSON) quote rule text that
|
|
1683
|
+
# the secret rules would match; they are not project code.
|
|
1684
|
+
if os.path.isdir(target_path):
|
|
1685
|
+
all_findings = own_reports.drop_own_reports(all_findings, target_path)
|
|
1686
|
+
if own_reports.SKIPPED:
|
|
1687
|
+
names = ", ".join(sorted(own_reports.SKIPPED)[:5])
|
|
1688
|
+
more = len(own_reports.SKIPPED) - 5
|
|
1689
|
+
console.print(f"[dim]Ignored Narvy report file(s) from an earlier run: {rich_escape(names)}"
|
|
1690
|
+
f"{f' and {more} more' if more > 0 else ''}.[/dim]")
|
|
1691
|
+
|
|
1640
1692
|
# Say so when a hybrid repo (RN/Flutter/Capacitor) has code this mode
|
|
1641
1693
|
# didn't cover, instead of exiting 0 silently.
|
|
1642
1694
|
uncovered_notes = _uncovered_surface_notes(target_path, scan_mode,
|
|
@@ -1655,6 +1707,9 @@ def cmd_scan(args):
|
|
|
1655
1707
|
if uncovered_notes:
|
|
1656
1708
|
scan_meta = dict(scan_meta)
|
|
1657
1709
|
scan_meta["uncovered_surfaces"] = uncovered_notes
|
|
1710
|
+
if unreadable:
|
|
1711
|
+
scan_meta = dict(scan_meta)
|
|
1712
|
+
scan_meta["unreadable_paths"] = unreadable
|
|
1658
1713
|
|
|
1659
1714
|
if args.output in ('sarif', 'json'):
|
|
1660
1715
|
# Written to a file, never stdout: progress notes would interleave.
|
|
@@ -1736,6 +1791,15 @@ def cmd_scan(args):
|
|
|
1736
1791
|
for note in uncovered_notes:
|
|
1737
1792
|
console.print(f"[bold yellow] - {note}[/bold yellow]")
|
|
1738
1793
|
|
|
1794
|
+
if unreadable:
|
|
1795
|
+
console.print(
|
|
1796
|
+
f"\n[bold yellow]{len(unreadable)} file(s) or folder(s) could not be read (permissions) "
|
|
1797
|
+
"and were not scanned.[/bold yellow]"
|
|
1798
|
+
+ ("" if getattr(args, "verbose", False) else " [dim]Add -v to list them.[/dim]"))
|
|
1799
|
+
if getattr(args, "verbose", False):
|
|
1800
|
+
for rel in unreadable:
|
|
1801
|
+
console.print(f"[yellow] - {rich_escape(rel)}[/yellow]")
|
|
1802
|
+
|
|
1739
1803
|
_premium_note = web_source_analyzer.premium_checks_note(premium_counts)
|
|
1740
1804
|
if _premium_note:
|
|
1741
1805
|
console.print(f"\n[dim]{rich_escape(_premium_note)}[/dim]")
|
|
@@ -1766,7 +1830,7 @@ def cmd_scan(args):
|
|
|
1766
1830
|
raise SystemExit(EXIT_BAD_TARGET)
|
|
1767
1831
|
upload_platform = "android" if scan_mode in ("android", "android-bundle") else "ios"
|
|
1768
1832
|
confirm_upload = getattr(args, "confirm_upload", False)
|
|
1769
|
-
console.print(f"\n[cyan]Uploading {os.path.basename(target_path)} to
|
|
1833
|
+
console.print(f"\n[cyan]Uploading {os.path.basename(target_path)} to {rich_escape(_server_and_account(creds))} for the full server-side scan...[/cyan]")
|
|
1770
1834
|
status, data = uploader.upload_scan(
|
|
1771
1835
|
target_path, creds["token"], platform=upload_platform, analysis_type="sast",
|
|
1772
1836
|
confirm_warning=confirm_upload,
|
|
@@ -1801,7 +1865,7 @@ def cmd_scan(args):
|
|
|
1801
1865
|
# The malware-triage gate returns a 'triage' dict alongside the
|
|
1802
1866
|
# top-level error on 400 (refuse) and 409 (warn).
|
|
1803
1867
|
error_msg = data.get('error') or data.get('warning') or data
|
|
1804
|
-
console.print(f"[bold red]
|
|
1868
|
+
console.print(f"[bold red]{_upload_failed_label(status)}[/bold red] {error_msg}")
|
|
1805
1869
|
_print_upgrade_hint(status, data)
|
|
1806
1870
|
triage = data.get('triage')
|
|
1807
1871
|
if isinstance(triage, dict):
|
|
@@ -1963,7 +2027,7 @@ def cmd_upload_all(args):
|
|
|
1963
2027
|
|
|
1964
2028
|
console.print(
|
|
1965
2029
|
f"[cyan]Found {len(targets)} target(s) under {directory}. "
|
|
1966
|
-
f"Scanning and uploading to {
|
|
2030
|
+
f"Scanning and uploading to {rich_escape(_server_and_account(creds))}...[/cyan]\n"
|
|
1967
2031
|
)
|
|
1968
2032
|
|
|
1969
2033
|
succeeded, failed = [], []
|
|
@@ -2159,7 +2223,12 @@ def cmd_web_scan(args):
|
|
|
2159
2223
|
try:
|
|
2160
2224
|
web_validate_url(args.url)
|
|
2161
2225
|
except SSRFBlocked as e:
|
|
2162
|
-
console.print(f"[bold red]ERROR:[/bold red] {e}")
|
|
2226
|
+
console.print(f"[bold red]ERROR:[/bold red] {rich_escape(str(e))}")
|
|
2227
|
+
if any(w in str(e) for w in ("loopback", "internal", "private")):
|
|
2228
|
+
console.print(
|
|
2229
|
+
"[dim]web-scan only scans publicly reachable URLs. Loopback, private-network and "
|
|
2230
|
+
"cloud metadata addresses are refused so a scan can never reach internal services. "
|
|
2231
|
+
"Point it at the app's public or staging URL.[/dim]")
|
|
2163
2232
|
raise SystemExit(EXIT_BAD_TARGET)
|
|
2164
2233
|
|
|
2165
2234
|
with scan_progress() as progress:
|
|
@@ -2186,7 +2255,7 @@ def cmd_web_scan(args):
|
|
|
2186
2255
|
findings = result.get("findings", [])
|
|
2187
2256
|
|
|
2188
2257
|
if args.format == "json":
|
|
2189
|
-
output = json.dumps(result, indent=2)
|
|
2258
|
+
output = json.dumps({"tool": "Narvy Web Scanner CLI", **result}, indent=2)
|
|
2190
2259
|
elif args.format == "sarif":
|
|
2191
2260
|
output = json.dumps(_web_to_sarif(findings), indent=2)
|
|
2192
2261
|
else:
|
|
@@ -2338,6 +2407,7 @@ def cmd_host_audit(args):
|
|
|
2338
2407
|
"Narvy internal host checks + Lynis (GPL-3.0) hardening baseline")
|
|
2339
2408
|
|
|
2340
2409
|
result = {
|
|
2410
|
+
"tool": "Narvy Host Audit CLI",
|
|
2341
2411
|
"target": f"{args.user}@{args.host}:{args.port}",
|
|
2342
2412
|
"duration_seconds": round(duration, 1),
|
|
2343
2413
|
"scan_engine": scan_engine,
|
|
@@ -2483,7 +2553,7 @@ def cmd_cloud_scan(args):
|
|
|
2483
2553
|
)
|
|
2484
2554
|
|
|
2485
2555
|
if args.format == "json":
|
|
2486
|
-
output = json.dumps(report, indent=2, default=str)
|
|
2556
|
+
output = json.dumps({"tool": "Narvy Cloud Scanner CLI", **report}, indent=2, default=str)
|
|
2487
2557
|
elif args.format == "sarif":
|
|
2488
2558
|
sarif = _cloud_to_sarif(findings)
|
|
2489
2559
|
sarif["runs"][0]["properties"] = {"coverage": coverage}
|
|
@@ -2640,7 +2710,11 @@ def cli():
|
|
|
2640
2710
|
p_scans.add_argument("--format", choices=["text", "json"], default="text", help="Output format (default: text)")
|
|
2641
2711
|
p_scans.set_defaults(func=cmd_scans)
|
|
2642
2712
|
|
|
2643
|
-
p_doctor = sub.add_parser(
|
|
2713
|
+
p_doctor = sub.add_parser(
|
|
2714
|
+
"doctor", allow_abbrev=False, help="Check your Java/jadx/network/iOS-tooling setup before scanning",
|
|
2715
|
+
epilog="exit codes:\n 0 every required check passed (missing optional tools are INFO only)\n"
|
|
2716
|
+
" 1 at least one required check failed",
|
|
2717
|
+
formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
2644
2718
|
p_doctor.set_defaults(func=lambda args: run_doctor())
|
|
2645
2719
|
|
|
2646
2720
|
p_scan = sub.add_parser(
|
|
@@ -2653,8 +2727,10 @@ def cli():
|
|
|
2653
2727
|
p_scan.add_argument("--max-mem", default="4g", help="Max memory for jadx, e.g. '2g' or '4096m' (default: 4g). Android only.")
|
|
2654
2728
|
p_scan.add_argument("--force", action="store_true",
|
|
2655
2729
|
help="Decompile anyway when the memory estimate says jadx will likely run out of memory.")
|
|
2656
|
-
p_scan.add_argument("--output", "-o", default="console", choices=["console", "sarif", "json"],
|
|
2730
|
+
p_scan.add_argument("--output", "-o", "--format", dest="output", default="console", choices=["console", "sarif", "json"],
|
|
2657
2731
|
type=str.lower, help="Output format: console (default), sarif or json. sarif and json are written to --file.")
|
|
2732
|
+
p_scan.add_argument("-v", "--verbose", action="store_true",
|
|
2733
|
+
help="List every file the scan could not read, not just the count.")
|
|
2658
2734
|
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.")
|
|
2659
2735
|
p_scan.add_argument("--upload", action="store_true", help="Send the findings (never your code or binary) to your Narvy dashboard after the scan. Needs `narvy login` and a paid plan.")
|
|
2660
2736
|
p_scan.add_argument("--upload-binary", action="store_true", help="Upload the binary itself (.apk/.aab/.ipa) for a server-side scan instead of the results. Implies --upload.")
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Recognize report files written by narvy itself, so a re-run never scans them."""
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
from typing import Dict, Optional, Set
|
|
5
|
+
|
|
6
|
+
# A real report of ours is far below this; bigger files are left to the rules.
|
|
7
|
+
_MAX_REPORT_BYTES = 256 * 1024 * 1024
|
|
8
|
+
|
|
9
|
+
_output_path: Optional[str] = None
|
|
10
|
+
_cache: Dict[str, bool] = {}
|
|
11
|
+
# Report files whose findings were dropped this run, for the end-of-scan note.
|
|
12
|
+
SKIPPED: Set[str] = set()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def set_output_path(path: Optional[str]) -> None:
|
|
16
|
+
"""The --file target of this run: whatever it holds now gets overwritten."""
|
|
17
|
+
global _output_path
|
|
18
|
+
_output_path = os.path.realpath(path) if path and path != "-" else None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _has_narvy_marker(data) -> bool:
|
|
22
|
+
if not isinstance(data, dict):
|
|
23
|
+
return False
|
|
24
|
+
tool = data.get("tool")
|
|
25
|
+
if isinstance(tool, str) and tool.startswith("Narvy"):
|
|
26
|
+
return True
|
|
27
|
+
runs = data.get("runs")
|
|
28
|
+
if isinstance(runs, list) and runs:
|
|
29
|
+
for run in runs:
|
|
30
|
+
tool = run.get("tool") if isinstance(run, dict) else None
|
|
31
|
+
driver = tool.get("driver") if isinstance(tool, dict) else None
|
|
32
|
+
name = driver.get("name") if isinstance(driver, dict) else None
|
|
33
|
+
if not (isinstance(name, str) and name.startswith("Narvy")):
|
|
34
|
+
return False
|
|
35
|
+
return True
|
|
36
|
+
return False
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def is_own_report(path: str) -> bool:
|
|
40
|
+
"""True for this run's --file target or a JSON/SARIF report narvy wrote."""
|
|
41
|
+
try:
|
|
42
|
+
real = os.path.realpath(path)
|
|
43
|
+
except (OSError, ValueError):
|
|
44
|
+
return False
|
|
45
|
+
if _output_path and real == _output_path:
|
|
46
|
+
return True
|
|
47
|
+
if real in _cache:
|
|
48
|
+
return _cache[real]
|
|
49
|
+
result = False
|
|
50
|
+
try:
|
|
51
|
+
if os.path.isfile(real) and os.path.getsize(real) <= _MAX_REPORT_BYTES:
|
|
52
|
+
with open(real, "rb") as f:
|
|
53
|
+
if f.read(256).lstrip(b"\xef\xbb\xbf \t\r\n").startswith(b"{"):
|
|
54
|
+
f.seek(0)
|
|
55
|
+
result = _has_narvy_marker(json.load(f))
|
|
56
|
+
except (OSError, ValueError, RecursionError):
|
|
57
|
+
result = False
|
|
58
|
+
_cache[real] = result
|
|
59
|
+
return result
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def drop_own_reports(findings, target_dir: str):
|
|
63
|
+
"""Findings minus those inside narvy report files, for a source-directory scan."""
|
|
64
|
+
kept = []
|
|
65
|
+
for f in findings:
|
|
66
|
+
fp = f.get("file_path") or ""
|
|
67
|
+
full = fp if os.path.isabs(fp) else os.path.join(target_dir, fp)
|
|
68
|
+
if fp and is_own_report(full):
|
|
69
|
+
SKIPPED.add(os.path.relpath(full, target_dir))
|
|
70
|
+
else:
|
|
71
|
+
kept.append(f)
|
|
72
|
+
return kept
|
|
@@ -2017,6 +2017,73 @@ rules:
|
|
|
2017
2017
|
...
|
|
2018
2018
|
|
|
2019
2019
|
'
|
|
2020
|
+
- id: narvy.python.session.flask-set-cookie-missing-httponly.flask-set-cookie-missing-httponly
|
|
2021
|
+
message: '`set_cookie()` is called without `httponly=True`. A cookie without HttpOnly is readable from
|
|
2022
|
+
JavaScript via `document.cookie`, so any XSS on the page can exfiltrate it (session hijack). CWE-1004.
|
|
2023
|
+
Set `httponly=True`, or `SESSION_COOKIE_HTTPONLY = True` globally.'
|
|
2024
|
+
severity: WARNING
|
|
2025
|
+
languages:
|
|
2026
|
+
- python
|
|
2027
|
+
metadata:
|
|
2028
|
+
cwe:
|
|
2029
|
+
- 'CWE-1004: Sensitive Cookie Without ''HttpOnly'' Flag'
|
|
2030
|
+
owasp:
|
|
2031
|
+
- A05:2021 - Security Misconfiguration
|
|
2032
|
+
category: security
|
|
2033
|
+
subcategory:
|
|
2034
|
+
- vuln
|
|
2035
|
+
technology:
|
|
2036
|
+
- flask
|
|
2037
|
+
likelihood: MEDIUM
|
|
2038
|
+
impact: MEDIUM
|
|
2039
|
+
confidence: MEDIUM
|
|
2040
|
+
vulnerability_class:
|
|
2041
|
+
- Cookie Security
|
|
2042
|
+
patterns:
|
|
2043
|
+
- pattern-either:
|
|
2044
|
+
- pattern-inside: |
|
|
2045
|
+
import flask
|
|
2046
|
+
...
|
|
2047
|
+
- pattern-inside: |
|
|
2048
|
+
from flask import $FLASK_NAME
|
|
2049
|
+
...
|
|
2050
|
+
- pattern: $RESP.set_cookie($NAME, $VALUE, ...)
|
|
2051
|
+
- pattern-not: $RESP.set_cookie($NAME, $VALUE, ..., httponly=True, ...)
|
|
2052
|
+
- pattern-not: $RESP.set_cookie($NAME, $VALUE, ..., httponly=$X, ...)
|
|
2053
|
+
- id: narvy.python.session.flask-set-cookie-missing-secure.flask-set-cookie-missing-secure
|
|
2054
|
+
message: '`set_cookie()` is called without `secure=True`. Without the Secure flag, the browser will
|
|
2055
|
+
also send this cookie over plain HTTP, exposing it to network eavesdroppers and active MITM (CWE-614).
|
|
2056
|
+
Set `secure=True` for any cookie carrying session/auth state, or set `SESSION_COOKIE_SECURE = True`
|
|
2057
|
+
globally in Flask config.'
|
|
2058
|
+
severity: WARNING
|
|
2059
|
+
languages:
|
|
2060
|
+
- python
|
|
2061
|
+
metadata:
|
|
2062
|
+
cwe:
|
|
2063
|
+
- 'CWE-614: Sensitive Cookie in HTTPS Session Without ''Secure'' Attribute'
|
|
2064
|
+
owasp:
|
|
2065
|
+
- A05:2021 - Security Misconfiguration
|
|
2066
|
+
category: security
|
|
2067
|
+
subcategory:
|
|
2068
|
+
- vuln
|
|
2069
|
+
technology:
|
|
2070
|
+
- flask
|
|
2071
|
+
likelihood: MEDIUM
|
|
2072
|
+
impact: MEDIUM
|
|
2073
|
+
confidence: MEDIUM
|
|
2074
|
+
vulnerability_class:
|
|
2075
|
+
- Cookie Security
|
|
2076
|
+
patterns:
|
|
2077
|
+
- pattern-either:
|
|
2078
|
+
- pattern-inside: |
|
|
2079
|
+
import flask
|
|
2080
|
+
...
|
|
2081
|
+
- pattern-inside: |
|
|
2082
|
+
from flask import $FLASK_NAME
|
|
2083
|
+
...
|
|
2084
|
+
- pattern: $RESP.set_cookie($NAME, $VALUE, ...)
|
|
2085
|
+
- pattern-not: $RESP.set_cookie($NAME, $VALUE, ..., secure=True, ...)
|
|
2086
|
+
- pattern-not: $RESP.set_cookie($NAME, $VALUE, ..., secure=$X, ...)
|
|
2020
2087
|
- id: narvy.python.sqli.pymongo-eval-where-injection.pymongo-eval-where-injection
|
|
2021
2088
|
message: MongoDB `$where` clause (or `db.eval()`) built with a tainted string. `$where` executes JavaScript
|
|
2022
2089
|
server-side against every document; an attacker-controlled fragment here is server-side NoSQL code
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import os
|
|
2
|
+
from urllib.parse import urlparse
|
|
2
3
|
|
|
3
4
|
import requests
|
|
4
5
|
|
|
@@ -6,6 +7,11 @@ import requests
|
|
|
6
7
|
API_BASE = (os.environ.get("NARVY_URL") or "https://narvy.io").rstrip("/")
|
|
7
8
|
|
|
8
9
|
|
|
10
|
+
def server_host():
|
|
11
|
+
"""Host the CLI talks to, for messages (narvy.io unless NARVY_URL says otherwise)."""
|
|
12
|
+
return urlparse(API_BASE).netloc or API_BASE
|
|
13
|
+
|
|
14
|
+
|
|
9
15
|
def _network_error(exc):
|
|
10
16
|
"""Return the (status, data) shape callers handle; status 0 never matches a real HTTP code."""
|
|
11
17
|
return 0, {
|
|
@@ -9,7 +9,7 @@ from typing import Any, Dict, List, Optional, Set, Tuple
|
|
|
9
9
|
|
|
10
10
|
import yaml
|
|
11
11
|
|
|
12
|
-
from narvy import semgrep_engine
|
|
12
|
+
from narvy import own_reports, semgrep_engine
|
|
13
13
|
from narvy.proc import run_tree
|
|
14
14
|
|
|
15
15
|
_HERE = os.path.dirname(__file__)
|
|
@@ -43,7 +43,7 @@ def pro_rules_available() -> bool:
|
|
|
43
43
|
# can say what it didn't run.
|
|
44
44
|
PREMIUM_CHECKS: Dict[str, Tuple[str, int]] = {
|
|
45
45
|
"django": ("Django / Django REST framework", 24),
|
|
46
|
-
"flask": ("Flask",
|
|
46
|
+
"flask": ("Flask", 10),
|
|
47
47
|
"fastapi": ("FastAPI", 1),
|
|
48
48
|
"express": ("Express", 2),
|
|
49
49
|
"laravel": ("Laravel", 7),
|
|
@@ -735,6 +735,9 @@ def _parse_semgrep_data(data: Dict[str, Any], source_dir: str,
|
|
|
735
735
|
skipped_generated += 1
|
|
736
736
|
continue
|
|
737
737
|
rel_path = os.path.relpath(abs_path, source_dir)
|
|
738
|
+
if own_reports.is_own_report(abs_path):
|
|
739
|
+
own_reports.SKIPPED.add(rel_path)
|
|
740
|
+
continue
|
|
738
741
|
extra = r.get("extra", {})
|
|
739
742
|
meta = extra.get("metadata", {})
|
|
740
743
|
check_id = r.get("check_id", "semgrep-finding").rsplit(".", 1)[-1]
|
|
@@ -154,6 +154,8 @@ def validate_url(url: str) -> Tuple[str, str, int]:
|
|
|
154
154
|
except SSRFBlocked:
|
|
155
155
|
raise
|
|
156
156
|
|
|
157
|
+
if hostname == 'localhost':
|
|
158
|
+
raise SSRFBlocked("localhost is this machine (loopback); scanning it is not allowed")
|
|
157
159
|
if hostname in METADATA_HOSTNAMES:
|
|
158
160
|
raise SSRFBlocked("Scanning internal/metadata addresses is not allowed")
|
|
159
161
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: narvy-cli
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.1
|
|
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
|
|
@@ -143,6 +143,9 @@ command. `narvy scans` lists recent scans on your account from the terminal.
|
|
|
143
143
|
| 4 | The scan, login, upload or report write failed |
|
|
144
144
|
| 130 | Interrupted (Ctrl-C, SIGTERM) |
|
|
145
145
|
|
|
146
|
+
`narvy doctor` exits 0 when every required check passes and 1 when one fails.
|
|
147
|
+
Missing optional tools are shown as INFO and do not change its exit code.
|
|
148
|
+
|
|
146
149
|
### Resource limits
|
|
147
150
|
|
|
148
151
|
The structural analysis pass runs with at most 4 parallel workers and a
|
|
@@ -165,7 +168,7 @@ Exactly these fields are sent:
|
|
|
165
168
|
|-------|---------|
|
|
166
169
|
| `schema` | `1`, the event format version |
|
|
167
170
|
| `install_id` | a random UUID created on first run and stored in `~/.narvy/config.json` (not derived from your machine, user or account) |
|
|
168
|
-
| `cli_version`, `python_version`, `os` | `1.1.
|
|
171
|
+
| `cli_version`, `python_version`, `os` | `1.1.1`, `3.11`, `linux` / `darwin` / `windows` / `other` |
|
|
169
172
|
| `ci` | `true` when a CI environment variable is set |
|
|
170
173
|
| `command` | `scan`, `web-scan`, `host-audit`, ... |
|
|
171
174
|
| `surface` | `android`, `ios`, `source`, `web`, `host`, `cloud` or `none` |
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|