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.
- {narvy_cli-1.0.1/narvy_cli.egg-info → narvy_cli-1.0.3}/PKG-INFO +1 -1
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/__init__.py +1 -1
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/android/source_analyzer.py +99 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/apk_memory_preflight.py +3 -3
- narvy_cli-1.0.3/narvy/ci_templates/bitbucket.yml +15 -0
- narvy_cli-1.0.3/narvy/ci_templates/github.yml +48 -0
- narvy_cli-1.0.3/narvy/ci_templates/gitlab.yml +19 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/doctor.py +6 -6
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/ios/binary_analyzer.py +10 -20
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/main.py +304 -43
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/rules/android.yml +115 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/rules/ios_swift.yml +34 -1
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/rules/web/python.yml +3 -5
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/rules/web/secrets.yml +9 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/rules/web/secrets_supplement.yml +8 -4
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/sca/web_deps.py +205 -4
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/semgrep_engine.py +10 -10
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/web/scanner.py +7 -7
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/web/source_analyzer.py +277 -55
- {narvy_cli-1.0.1 → narvy_cli-1.0.3/narvy_cli.egg-info}/PKG-INFO +1 -1
- narvy_cli-1.0.1/narvy/ci_templates/bitbucket.yml +0 -25
- narvy_cli-1.0.1/narvy/ci_templates/github.yml +0 -60
- narvy_cli-1.0.1/narvy/ci_templates/gitlab.yml +0 -32
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/CHANGELOG.md +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/LICENSE +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/MANIFEST.in +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/README.md +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/SECURITY.md +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/android/__init__.py +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/android/split_bundle.py +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/android_rule_context.py +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/auth.py +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/cloud/__init__.py +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/cloud/aws_scan.py +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/comment_filter.py +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/crypto_taint_lite.py +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/decompiler.py +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/host/__init__.py +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/host/audit.py +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/host/host_knowledge.py +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/host/lynis_bootstrap.py +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/host/lynis_parser.py +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/host/narvy_checks.py +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/host/report.py +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/host/ssh_exec.py +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/ios/__init__.py +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/ios/plist_checks.py +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/ios/source_analyzer.py +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/ios/third_party_filter.py +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/ios/trust_all_context.py +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/native_hardening.py +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/reporter.py +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/rule_engine.py +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/rules/android/config.yml +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/rules/android/crypto.yml +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/rules/android/secrets.yml +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/rules/android/storage.yml +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/rules/android/webview.yml +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/rules/ios/objc/crypto.yml +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/rules/ios/objc/network.yml +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/rules/ios/objc/secrets.yml +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/rules/ios/objc/storage.yml +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/rules/ios/objc/webview.yml +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/rules/web/go.yml +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/rules/web/java.yml +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/rules/web/javascript.yml +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/rules/web/kotlin.yml +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/rules/web/local/csharp_narvy/config.yml +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/rules/web/local/csharp_narvy/crypto.yml +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/rules/web/local/csharp_narvy/deserialization.yml +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/rules/web/local/csharp_narvy/injection.yml +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/rules/web/local/csharp_narvy/xxe.yml +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/rules/web/local/java_narvy/auth_jwt.yml +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/rules/web/local/java_narvy/deserialization.yml +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/rules/web/local/java_narvy/mybatis.yml +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/rules/web/local/java_narvy/snakeyaml.yml +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/rules/web/local/java_narvy/spring_authz.yml +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/rules/web/local/java_narvy/spring_config.yml +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/rules/web/local/java_narvy/spring_hardening.yml +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/rules/web/local/java_narvy/sqli.yml +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/rules/web/local/java_narvy/xxe.yml +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/rules/web/php.yml +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/rules/web/ruby.yml +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/rules/web/rust.yml +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/sca/__init__.py +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/sca/android_deps.py +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/sca/ios_deps.py +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/sca/osv_client.py +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/scope_config.py +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/stack_protector_evidence.py +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/third_party_filter.py +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/uploader.py +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/weak_prng_context.py +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/web/__init__.py +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/web/nuclei_binary.py +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/web/scan_blocklist.py +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy/web/ssrf_guard.py +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy_cli.egg-info/SOURCES.txt +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy_cli.egg-info/dependency_links.txt +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy_cli.egg-info/entry_points.txt +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy_cli.egg-info/requires.txt +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/narvy_cli.egg-info/top_level.txt +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/pyproject.toml +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/setup.cfg +0 -0
- {narvy_cli-1.0.1 → narvy_cli-1.0.3}/setup.py +0 -0
|
@@ -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" -
|
|
213
|
-
f" -
|
|
214
|
-
f" - run it anyway with --force if
|
|
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
|
|
114
|
-
"
|
|
115
|
-
"
|
|
116
|
-
"
|
|
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
|
|
1025
|
-
"
|
|
1026
|
-
"
|
|
1027
|
-
"
|
|
1028
|
-
"
|
|
1029
|
-
"
|
|
1030
|
-
"
|
|
1031
|
-
"run
|
|
1032
|
-
"
|
|
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
|