narvy-cli 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. narvy/__init__.py +3 -0
  2. narvy/android/__init__.py +0 -0
  3. narvy/android/source_analyzer.py +268 -0
  4. narvy/android/split_bundle.py +475 -0
  5. narvy/android_rule_context.py +196 -0
  6. narvy/apk_memory_preflight.py +248 -0
  7. narvy/auth.py +40 -0
  8. narvy/ci_templates/bitbucket.yml +25 -0
  9. narvy/ci_templates/github.yml +60 -0
  10. narvy/ci_templates/gitlab.yml +32 -0
  11. narvy/cloud/__init__.py +0 -0
  12. narvy/cloud/aws_scan.py +1188 -0
  13. narvy/comment_filter.py +134 -0
  14. narvy/crypto_taint_lite.py +82 -0
  15. narvy/decompiler.py +337 -0
  16. narvy/doctor.py +244 -0
  17. narvy/host/__init__.py +0 -0
  18. narvy/host/audit.py +157 -0
  19. narvy/host/host_knowledge.py +982 -0
  20. narvy/host/lynis_bootstrap.py +400 -0
  21. narvy/host/lynis_parser.py +358 -0
  22. narvy/host/narvy_checks.py +789 -0
  23. narvy/host/report.py +69 -0
  24. narvy/host/ssh_exec.py +219 -0
  25. narvy/ios/__init__.py +0 -0
  26. narvy/ios/binary_analyzer.py +1201 -0
  27. narvy/ios/plist_checks.py +249 -0
  28. narvy/ios/source_analyzer.py +301 -0
  29. narvy/ios/third_party_filter.py +242 -0
  30. narvy/ios/trust_all_context.py +66 -0
  31. narvy/main.py +1983 -0
  32. narvy/native_hardening.py +503 -0
  33. narvy/reporter.py +151 -0
  34. narvy/rule_engine.py +57 -0
  35. narvy/rules/android/config.yml +77 -0
  36. narvy/rules/android/crypto.yml +34 -0
  37. narvy/rules/android/secrets.yml +161 -0
  38. narvy/rules/android/storage.yml +24 -0
  39. narvy/rules/android/webview.yml +23 -0
  40. narvy/rules/android.yml +219 -0
  41. narvy/rules/ios/objc/crypto.yml +56 -0
  42. narvy/rules/ios/objc/network.yml +67 -0
  43. narvy/rules/ios/objc/secrets.yml +79 -0
  44. narvy/rules/ios/objc/storage.yml +45 -0
  45. narvy/rules/ios/objc/webview.yml +45 -0
  46. narvy/rules/ios_swift.yml +327 -0
  47. narvy/rules/web/go.yml +2837 -0
  48. narvy/rules/web/java.yml +1576 -0
  49. narvy/rules/web/javascript.yml +3683 -0
  50. narvy/rules/web/kotlin.yml +413 -0
  51. narvy/rules/web/local/csharp_narvy/config.yml +61 -0
  52. narvy/rules/web/local/csharp_narvy/crypto.yml +64 -0
  53. narvy/rules/web/local/csharp_narvy/deserialization.yml +59 -0
  54. narvy/rules/web/local/csharp_narvy/injection.yml +122 -0
  55. narvy/rules/web/local/csharp_narvy/xxe.yml +48 -0
  56. narvy/rules/web/local/java_narvy/auth_jwt.yml +134 -0
  57. narvy/rules/web/local/java_narvy/deserialization.yml +108 -0
  58. narvy/rules/web/local/java_narvy/mybatis.yml +39 -0
  59. narvy/rules/web/local/java_narvy/snakeyaml.yml +34 -0
  60. narvy/rules/web/local/java_narvy/spring_authz.yml +32 -0
  61. narvy/rules/web/local/java_narvy/spring_config.yml +67 -0
  62. narvy/rules/web/local/java_narvy/spring_hardening.yml +291 -0
  63. narvy/rules/web/local/java_narvy/sqli.yml +235 -0
  64. narvy/rules/web/local/java_narvy/xxe.yml +212 -0
  65. narvy/rules/web/php.yml +1644 -0
  66. narvy/rules/web/python.yml +3967 -0
  67. narvy/rules/web/ruby.yml +703 -0
  68. narvy/rules/web/rust.yml +258 -0
  69. narvy/rules/web/secrets.yml +1420 -0
  70. narvy/rules/web/secrets_supplement.yml +383 -0
  71. narvy/sca/__init__.py +1 -0
  72. narvy/sca/android_deps.py +349 -0
  73. narvy/sca/ios_deps.py +578 -0
  74. narvy/sca/osv_client.py +617 -0
  75. narvy/sca/web_deps.py +955 -0
  76. narvy/scope_config.py +195 -0
  77. narvy/semgrep_engine.py +219 -0
  78. narvy/stack_protector_evidence.py +96 -0
  79. narvy/third_party_filter.py +211 -0
  80. narvy/uploader.py +92 -0
  81. narvy/weak_prng_context.py +270 -0
  82. narvy/web/__init__.py +0 -0
  83. narvy/web/nuclei_binary.py +105 -0
  84. narvy/web/scan_blocklist.py +96 -0
  85. narvy/web/scanner.py +842 -0
  86. narvy/web/source_analyzer.py +714 -0
  87. narvy/web/ssrf_guard.py +374 -0
  88. narvy_cli-1.0.0.dist-info/METADATA +165 -0
  89. narvy_cli-1.0.0.dist-info/RECORD +93 -0
  90. narvy_cli-1.0.0.dist-info/WHEEL +5 -0
  91. narvy_cli-1.0.0.dist-info/entry_points.txt +2 -0
  92. narvy_cli-1.0.0.dist-info/licenses/LICENSE +202 -0
  93. narvy_cli-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,248 @@
1
+ """Pre-flight memory estimate for the JADX decompilation path: estimate the Java
2
+ heap jadx needs from DEX header counts and warn before OOM. Advisory only; `--force` overrides."""
3
+
4
+ import ctypes
5
+ import logging
6
+ import math
7
+ import os
8
+ import platform
9
+ import re
10
+ import struct
11
+ import zipfile
12
+ from typing import NamedTuple, Optional
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ # Class count is the discriminating estimator; the other two add caution under max().
17
+ HEAP_MB_PER_CLASS = 0.050
18
+ HEAP_MB_PER_METHOD = 0.0073
19
+ HEAP_MB_PER_DEX_MB = 45.0
20
+
21
+ MIN_ESTIMATE_MB = 512
22
+
23
+ # JVM resident set = heap plus metaspace, stacks, GC, JIT cache, mapped DEX.
24
+ JVM_RSS_OVERHEAD = 1.30
25
+
26
+ RAM_SAFETY_RESERVE_MB = 1024
27
+
28
+
29
+ class DexStats(NamedTuple):
30
+ methods: int
31
+ classes: int
32
+ dex_files: int
33
+ dex_bytes: int
34
+ ok: bool # False => header read failed, callers must not gate on this
35
+
36
+
37
+ class PreflightVerdict(NamedTuple):
38
+ should_block: bool
39
+ reason: str # "" | "heap_too_small" | "ram_too_small"
40
+ message: str
41
+ estimated_mb: int
42
+ max_mem_mb: int
43
+ available_mb: Optional[int]
44
+ stats: DexStats
45
+
46
+
47
+ def parse_mem_arg(value: str) -> Optional[int]:
48
+ """Parse a JVM-style memory string ('4g', '4096m', '512k') to MB, or None."""
49
+ if not value:
50
+ return None
51
+ m = re.fullmatch(r"\s*(\d+)\s*([kmgKMG]?)[bB]?\s*", str(value))
52
+ if not m:
53
+ return None
54
+ n = int(m.group(1))
55
+ unit = m.group(2).lower()
56
+ if unit == "g":
57
+ return n * 1024
58
+ if unit == "m":
59
+ return n
60
+ if unit == "k":
61
+ return max(1, n // 1024)
62
+ return max(1, n // (1024 * 1024)) # bare number = bytes, per java -Xmx
63
+
64
+
65
+ def available_ram_mb() -> Optional[int]:
66
+ """Best-effort free memory in MB for this machine, or None if unknown."""
67
+ system = platform.system()
68
+ try:
69
+ if system == "Linux":
70
+ with open("/proc/meminfo", "r") as f:
71
+ for line in f:
72
+ if line.startswith("MemAvailable:"):
73
+ return int(line.split()[1]) // 1024
74
+ elif system == "Darwin":
75
+ # vm_stat exposes reclaimable inactive+purgeable pools that SC_AVPHYS_PAGES omits.
76
+ import subprocess
77
+ out = subprocess.run(["vm_stat"], capture_output=True, text=True, timeout=10).stdout
78
+ page = 4096
79
+ pm = re.search(r"page size of (\d+) bytes", out)
80
+ if pm:
81
+ page = int(pm.group(1))
82
+ counts = dict(re.findall(r"^(.+?):\s+(\d+)\.", out, re.M))
83
+ free = int(counts.get("Pages free", 0))
84
+ inactive = int(counts.get("Pages inactive", 0))
85
+ purgeable = int(counts.get("Pages purgeable", 0))
86
+ if free or inactive:
87
+ return ((free + inactive + purgeable) * page) // (1024 * 1024)
88
+ elif system == "Windows":
89
+ class _MemStatusEx(ctypes.Structure):
90
+ _fields_ = [
91
+ ("dwLength", ctypes.c_ulong),
92
+ ("dwMemoryLoad", ctypes.c_ulong),
93
+ ("ullTotalPhys", ctypes.c_ulonglong),
94
+ ("ullAvailPhys", ctypes.c_ulonglong),
95
+ ("ullTotalPageFile", ctypes.c_ulonglong),
96
+ ("ullAvailPageFile", ctypes.c_ulonglong),
97
+ ("ullTotalVirtual", ctypes.c_ulonglong),
98
+ ("ullAvailVirtual", ctypes.c_ulonglong),
99
+ ("ullAvailExtendedVirtual", ctypes.c_ulonglong),
100
+ ]
101
+ stat = _MemStatusEx()
102
+ stat.dwLength = ctypes.sizeof(_MemStatusEx)
103
+ if ctypes.windll.kernel32.GlobalMemoryStatusEx(ctypes.byref(stat)):
104
+ return int(stat.ullAvailPhys) // (1024 * 1024)
105
+ # Generic POSIX fallback, also covers Linux if /proc is unreadable.
106
+ if hasattr(os, "sysconf") and "SC_AVPHYS_PAGES" in os.sysconf_names:
107
+ return (os.sysconf("SC_AVPHYS_PAGES") * os.sysconf("SC_PAGE_SIZE")) // (1024 * 1024)
108
+ except Exception as e: # noqa: BLE001
109
+ logger.debug(f"available_ram_mb() failed on {system}: {e}")
110
+ return None
111
+
112
+
113
+ def _is_app_dex(name: str) -> bool:
114
+ """True for app-code DEX (APK `classes*.dex` or App Bundle `<module>/dex/classes*.dex`), not asset .dex."""
115
+ base = name.rsplit("/", 1)[-1]
116
+ if not (base.startswith("classes") and base.endswith(".dex")):
117
+ return False
118
+ depth = name.count("/")
119
+ if depth == 0:
120
+ return True
121
+ return re.fullmatch(r"[^/]+/dex/classes\d*\.dex", name) is not None
122
+
123
+
124
+ def read_dex_stats(apk_path: str) -> DexStats:
125
+ """Read method/class counts from DEX headers (Dalvik spec: method_ids_size @0x58, class_defs_size @0x60, uint32 LE)."""
126
+ methods = classes = count = 0
127
+ dex_bytes = 0
128
+ try:
129
+ with zipfile.ZipFile(apk_path) as zf:
130
+ for zi in zf.infolist():
131
+ if not _is_app_dex(zi.filename):
132
+ continue
133
+ try:
134
+ with zf.open(zi) as f:
135
+ hdr = f.read(112)
136
+ except Exception:
137
+ continue
138
+ if len(hdr) < 0x70 or hdr[:3] != b"dex":
139
+ continue
140
+ m = struct.unpack_from("<I", hdr, 0x58)[0]
141
+ c = struct.unpack_from("<I", hdr, 0x60)[0]
142
+ # Corrupt-header guard: no real DEX holds these counts.
143
+ if m > 5_000_000 or c > 1_000_000:
144
+ continue
145
+ methods += m
146
+ classes += c
147
+ dex_bytes += zi.file_size
148
+ count += 1
149
+ except Exception as e: # noqa: BLE001
150
+ logger.debug(f"read_dex_stats({apk_path}) failed: {e}")
151
+ return DexStats(0, 0, 0, 0, ok=False)
152
+ return DexStats(methods, classes, count, dex_bytes, ok=count > 0)
153
+
154
+
155
+ def estimate_heap_mb(stats: DexStats) -> int:
156
+ """Estimated -Xmx (MB) jadx needs to decompile an app of this size."""
157
+ if not stats.ok:
158
+ return 0
159
+ estimates = (
160
+ stats.classes * HEAP_MB_PER_CLASS,
161
+ stats.methods * HEAP_MB_PER_METHOD,
162
+ (stats.dex_bytes / (1024 * 1024)) * HEAP_MB_PER_DEX_MB,
163
+ )
164
+ return max(MIN_ESTIMATE_MB, int(max(estimates)))
165
+
166
+
167
+ def _fmt_mb(mb: Optional[int]) -> str:
168
+ if mb is None:
169
+ return "unknown"
170
+ if mb >= 1024:
171
+ return f"{mb / 1024:.1f} GB"
172
+ return f"{mb} MB"
173
+
174
+
175
+ def _round_up_gb(mb: int) -> int:
176
+ return max(1, math.ceil(mb / 1024))
177
+
178
+
179
+ def check_memory_preflight(apk_path: str, max_mem: str,
180
+ available_mb: Optional[int] = None) -> PreflightVerdict:
181
+ """Decide before spending jadx time whether this scan can work; never blocks on a low-confidence estimate."""
182
+ stats = read_dex_stats(apk_path)
183
+ max_mem_mb = parse_mem_arg(max_mem)
184
+ if available_mb is None:
185
+ available_mb = available_ram_mb()
186
+
187
+ no_block = PreflightVerdict(False, "", "", estimate_heap_mb(stats),
188
+ max_mem_mb or 0, available_mb, stats)
189
+ if not stats.ok or not max_mem_mb:
190
+ return no_block
191
+
192
+ need_mb = estimate_heap_mb(stats)
193
+ scale = (
194
+ f"~{stats.methods:,} methods / ~{stats.classes:,} classes across "
195
+ f"{stats.dex_files} DEX file(s), {stats.dex_bytes / (1024 * 1024):.0f} MB of bytecode"
196
+ )
197
+
198
+ if need_mb > max_mem_mb:
199
+ suggest_gb = _round_up_gb(int(need_mb * 1.1))
200
+ need_rss_mb = int(suggest_gb * 1024 * JVM_RSS_OVERHEAD)
201
+ head = (
202
+ f"This app is very large: {scale}. Decompiling it typically needs around "
203
+ f"{_fmt_mb(need_mb)} of Java heap, but this scan is limited to "
204
+ f"{_fmt_mb(max_mem_mb)} (--max-mem {max_mem}). JADX will most likely run out "
205
+ f"of memory partway through, after several minutes of work."
206
+ )
207
+ if available_mb is not None and need_rss_mb > (available_mb - RAM_SAFETY_RESERVE_MB):
208
+ body = (
209
+ f"\n\nRaising --max-mem on its own will not be enough: {suggest_gb}g of heap needs "
210
+ f"roughly {_fmt_mb(need_rss_mb)} of real memory, and this machine only has "
211
+ f"{_fmt_mb(available_mb)} available right now. Options:\n"
212
+ f" - free up that much memory (close other apps), then re-run with --max-mem {suggest_gb}g\n"
213
+ f" - scan this app on a bigger machine, or use the hosted scan (`--upload`)\n"
214
+ f" - run it anyway with --force if you know your setup can take it"
215
+ )
216
+ else:
217
+ avail_note = (f" (this machine has {_fmt_mb(available_mb)} available)"
218
+ if available_mb is not None else "")
219
+ body = (
220
+ f"\n\nOptions:\n"
221
+ f" - re-run with --max-mem {suggest_gb}g{avail_note}\n"
222
+ f" - run it anyway at {max_mem} with --force"
223
+ )
224
+ return PreflightVerdict(True, "heap_too_small", head + body,
225
+ need_mb, max_mem_mb, available_mb, stats)
226
+
227
+ if available_mb is not None:
228
+ needed_rss = int(max_mem_mb * JVM_RSS_OVERHEAD)
229
+ if needed_rss > (available_mb - RAM_SAFETY_RESERVE_MB):
230
+ safe_gb = max(1, int((available_mb - RAM_SAFETY_RESERVE_MB) / JVM_RSS_OVERHEAD / 1024))
231
+ lower_line = f" - lower to --max-mem {safe_gb}g, which fits in what's free"
232
+ if safe_gb * 1024 < need_mb:
233
+ lower_line += (f" (though this app looks like it needs about {_fmt_mb(need_mb)}, "
234
+ f"so it may still not be enough)")
235
+ msg = (
236
+ f"--max-mem {max_mem} asks JADX for {_fmt_mb(max_mem_mb)} of Java heap, which needs "
237
+ f"about {_fmt_mb(needed_rss)} of real memory once JVM overhead is counted. This "
238
+ f"machine only has {_fmt_mb(available_mb)} available right now.\n\n"
239
+ f"Running this would push the machine into swap and most likely get JADX killed by "
240
+ f"the OS out-of-memory killer (and may take other apps down with it). Options:\n"
241
+ f" - free up memory and try again\n"
242
+ f"{lower_line}\n"
243
+ f" - run it anyway with --force"
244
+ )
245
+ return PreflightVerdict(True, "ram_too_small", msg,
246
+ need_mb, max_mem_mb, available_mb, stats)
247
+
248
+ return no_block
narvy/auth.py ADDED
@@ -0,0 +1,40 @@
1
+ import json
2
+ import os
3
+ import stat
4
+
5
+ CONFIG_DIR = os.path.expanduser("~/.narvy")
6
+ CREDENTIALS_PATH = os.path.join(CONFIG_DIR, "credentials")
7
+
8
+
9
+ def save_token(token, email, plan):
10
+ os.makedirs(CONFIG_DIR, exist_ok=True)
11
+ # 0600 at creation so the token file is never briefly readable by others.
12
+ fd = os.open(CREDENTIALS_PATH, os.O_CREAT | os.O_WRONLY | os.O_TRUNC, 0o600)
13
+ with os.fdopen(fd, "w") as f:
14
+ json.dump({"token": token, "email": email, "plan": plan}, f)
15
+ os.chmod(CREDENTIALS_PATH, stat.S_IRUSR | stat.S_IWUSR)
16
+
17
+
18
+ def _env(name):
19
+ """Read a NARVY_<name> environment override."""
20
+ return os.environ.get(f"NARVY_{name}", "").strip()
21
+
22
+
23
+ def load_credentials():
24
+ # NARVY_TOKEN overrides the stored file for CI / parallel use.
25
+ env_token = _env("TOKEN")
26
+ if env_token:
27
+ return {
28
+ "token": env_token,
29
+ "email": _env("EMAIL"),
30
+ "plan": _env("PLAN"),
31
+ }
32
+ if not os.path.exists(CREDENTIALS_PATH):
33
+ return None
34
+ with open(CREDENTIALS_PATH) as f:
35
+ return json.load(f)
36
+
37
+
38
+ def logout():
39
+ if os.path.exists(CREDENTIALS_PATH):
40
+ os.remove(CREDENTIALS_PATH)
@@ -0,0 +1,25 @@
1
+ # Narvy SAST - free security scanning in Bitbucket Pipelines.
2
+ # `narvy init-ci --bitbucket` writes this file. Safe to edit and commit.
3
+ #
4
+ # If you ALREADY have a bitbucket-pipelines.yml, do NOT overwrite it - copy the
5
+ # step below into your existing pipeline instead.
6
+ #
7
+ # What it does: runs the Narvy CLI over the repository, emits SARIF
8
+ # (stored as a build artifact), and fails the build when a finding is at or
9
+ # above the severity you choose with --fail-on.
10
+ #
11
+ # NOTE: to surface findings in Bitbucket's Code Insights panel you would POST
12
+ # the SARIF via the Bitbucket Reports API (an extra step / pipe), which this
13
+ # template does not do. The build gate (--fail-on) works as-is.
14
+ image: python:3.11
15
+
16
+ pipelines:
17
+ default:
18
+ - step:
19
+ name: Narvy SAST
20
+ script:
21
+ - pip install --upgrade pip
22
+ - pip install narvy
23
+ - narvy scan . --output sarif --file narvy-results.sarif --fail-on high
24
+ artifacts:
25
+ - narvy-results.sarif
@@ -0,0 +1,60 @@
1
+ # Narvy SAST - free security scanning in GitHub Actions.
2
+ # `narvy init-ci --github` writes this file. Safe to edit and commit.
3
+ #
4
+ # What it does: runs the Narvy CLI over this repository (mobile / web /
5
+ # backend source SAST + SCA), emits SARIF, uploads it to GitHub code scanning
6
+ # (the repo's Security tab), and fails the build when a finding is at or above
7
+ # the severity you choose with --fail-on.
8
+ name: Narvy SAST
9
+
10
+ on:
11
+ push:
12
+ branches: [ main, master ]
13
+ pull_request:
14
+ workflow_dispatch:
15
+
16
+ permissions:
17
+ contents: read
18
+ security-events: write # required to upload SARIF to code scanning
19
+
20
+ jobs:
21
+ narvy:
22
+ runs-on: ubuntu-latest
23
+ steps:
24
+ - uses: actions/checkout@v4
25
+
26
+ - uses: actions/setup-python@v5
27
+ with:
28
+ python-version: '3.11'
29
+
30
+ - name: Install Narvy CLI
31
+ run: |
32
+ python -m pip install --upgrade pip
33
+ pip install narvy
34
+
35
+ - name: Run Narvy scan
36
+ # The SARIF file is written before the --fail-on gate runs, so the
37
+ # Security tab is populated even on a failing build (see the upload
38
+ # step's `if: always()`).
39
+ run: |
40
+ narvy scan . \
41
+ --output sarif \
42
+ --file narvy-results.sarif \
43
+ --fail-on high
44
+
45
+ - name: Upload SARIF to GitHub code scanning
46
+ if: always() # upload even when the scan step failed the build via --fail-on
47
+ uses: github/codeql-action/upload-sarif@v3
48
+ with:
49
+ sarif_file: narvy-results.sarif
50
+
51
+ # ---------------------------------------------------------------------------
52
+ # Scanning a web app URL too? Add a second step (needs no account):
53
+ # - name: Narvy passive web scan
54
+ # run: narvy web-scan "https://staging.example.com" --format sarif --output-file web.sarif --fail-on high
55
+ # - if: always()
56
+ # uses: github/codeql-action/upload-sarif@v3
57
+ # with:
58
+ # sarif_file: web.sarif
59
+ # category: narvy-web
60
+ # ---------------------------------------------------------------------------
@@ -0,0 +1,32 @@
1
+ # Narvy SAST - free security scanning in GitLab CI.
2
+ # `narvy init-ci --gitlab` writes this file. Safe to edit and commit.
3
+ #
4
+ # If you ALREADY have a .gitlab-ci.yml, do NOT overwrite it - copy the
5
+ # `narvy_sast` job below into your existing pipeline instead.
6
+ #
7
+ # What it does: runs the Narvy CLI over the repository, emits SARIF,
8
+ # saves it as a job artifact, and fails the pipeline when a finding is at or
9
+ # above the severity you choose with --fail-on.
10
+ #
11
+ # NOTE on GitLab's Security Dashboard: native ingestion
12
+ # (artifacts:reports:sast) expects GitLab's OWN SAST report schema, which this
13
+ # CLI does not emit. The SARIF is stored as a plain downloadable artifact and
14
+ # the pipeline gate (--fail-on) still blocks the MR. Surfacing findings inside
15
+ # the Security tab would require converting SARIF -> GitLab SAST schema, which
16
+ # this template does not do.
17
+ stages:
18
+ - test
19
+
20
+ narvy_sast:
21
+ stage: test
22
+ image: python:3.11-slim
23
+ before_script:
24
+ - pip install --upgrade pip
25
+ - pip install narvy
26
+ script:
27
+ - narvy scan . --output sarif --file narvy-results.sarif --fail-on high
28
+ artifacts:
29
+ when: always # keep the SARIF even when --fail-on failed the pipeline
30
+ paths:
31
+ - narvy-results.sarif
32
+ expire_in: 1 week
File without changes