@murk-security/runner-darwin-x64 0.0.0-stage → 0.7.1
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.
- package/README.md +2 -2
- package/bin/harness +0 -0
- package/package.json +15 -4
- package/runners/basic_harness_run.py +1276 -0
- package/runners/checklists/anonymous.json +24 -0
- package/runners/cve_feed.json +53 -0
- package/runners/prod_env.py +1686 -0
- package/runners/profile.py +307 -0
- package/runners/recon.py +396 -0
- package/runners/remediate.py +429 -0
- package/runners/report.py +365 -0
- package/runners/skills/api/idor-enumeration/SKILL.md +42 -0
- package/runners/skills/exploit/authz-differential/SKILL.md +62 -0
- package/runners/skills/exploit/business-logic/SKILL.md +53 -0
- package/runners/skills/exploit/client-side-exploitation/SKILL.md +63 -0
- package/runners/skills/exploit/client-side-rendering/SKILL.md +57 -0
- package/runners/skills/exploit/credential-chaining/SKILL.md +56 -0
- package/runners/skills/exploit/known-cve-validation/SKILL.md +46 -0
- package/runners/skills/exploit/path-traversal/SKILL.md +44 -0
- package/runners/skills/exploit/reflected-xss/SKILL.md +43 -0
- package/runners/skills/exploit/service-exploitation/SKILL.md +64 -0
- package/runners/skills/exploit/service-probing/SKILL.md +51 -0
- package/runners/skills/exploit/sqli-inference/SKILL.md +46 -0
- package/runners/skills/exploit/ssrf-oob/SKILL.md +48 -0
- package/runners/skills/exploit/state-change-exploitation/SKILL.md +72 -0
- package/runners/skills/exploit/stateful-authz/SKILL.md +50 -0
- package/runners/skills/recon/subdomain-takeover/SKILL.md +45 -0
- package/runners/skills/web/content-discovery/SKILL.md +59 -0
- package/runners/skills/web/graphql-exposure/SKILL.md +40 -0
- package/runners/skills.py +174 -0
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Preliminary-scan ("profile") pre-pass -- Phase 0.
|
|
3
|
+
|
|
4
|
+
A DETERMINISTIC step that runs BEFORE the agent rounds and reuses prod_env's passive
|
|
5
|
+
machinery to produce a profile.json: fingerprint + security-posture FACTS + candidate
|
|
6
|
+
SIGNALS. It is a seed for the orchestrator, NOT a verdict engine -- every signal is an
|
|
7
|
+
observation to confirm, never a finding (see docs/preliminary-scan-plan.md).
|
|
8
|
+
|
|
9
|
+
Gating: this module does nothing unless a driver invokes it. prod_env consumes its
|
|
10
|
+
output ONLY when BLACKGLASS_PROFILE is enabled and BLACKGLASS_PROFILE_PATH points at the
|
|
11
|
+
file -- so with the feature off, the adapter is byte-identical to before.
|
|
12
|
+
|
|
13
|
+
Phase 0 scope: section B of the catalog (security headers / cookie flags / transport),
|
|
14
|
+
plus a light fingerprint. Passive, scope-bound (it only touches the origin, anonymously,
|
|
15
|
+
via prod_env._http). Later phases add exposed-path sweep, version->CVE, the notable-CVE
|
|
16
|
+
feed, safe-active signals, and the source/static track.
|
|
17
|
+
|
|
18
|
+
python3 profile.py --spec input-spec.json [--out profile.json] [--tier passive]
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import argparse
|
|
24
|
+
import json
|
|
25
|
+
import os
|
|
26
|
+
import re
|
|
27
|
+
import sys
|
|
28
|
+
import time
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
|
|
31
|
+
HERE = Path(__file__).resolve().parent
|
|
32
|
+
sys.path.insert(0, str(HERE))
|
|
33
|
+
import prod_env # reuse Target + _http (scope-bound, WAF-friendly fetch)
|
|
34
|
+
|
|
35
|
+
# Notable/recent-CVE feed (area: known-CVE / version->CVE track). A separate,
|
|
36
|
+
# refreshable JSON data file -- NOT code -- because flagship threats (PHP2Shell etc.)
|
|
37
|
+
# are always post-model-cutoff; point BLACKGLASS_CVE_FEED at a newer copy to update.
|
|
38
|
+
DEFAULT_CVE_FEED = HERE / "cve_feed.json"
|
|
39
|
+
# Matches "product/1.2.3" tokens in a Server / X-Powered-By banner (>=2 version parts).
|
|
40
|
+
_FP_RE = re.compile(r"([A-Za-z][A-Za-z0-9_+.-]*?)/(\d+(?:\.\d+)+)")
|
|
41
|
+
|
|
42
|
+
# Exposed-path sweep (catalog area C). A CAPPED list of high-signal paths, each with a
|
|
43
|
+
# content SIGNATURE (`must`) the 200 body has to match -- that gate is what gives
|
|
44
|
+
# precision: a soft-404 / SPA catch-all / decoy that returns 200 with unrelated content
|
|
45
|
+
# does NOT flag. Signatures distilled from the nuclei exposures/misconfiguration families
|
|
46
|
+
# (MIT) -- as knowledge, not by running the YAML DSL. Categories drive severity/confidence.
|
|
47
|
+
MAX_SWEEP_PATHS = 40
|
|
48
|
+
EXPOSED_PATHS = [
|
|
49
|
+
{"path": "/.git/config", "cat": "vcs", "must": r"\[core\]|repositoryformatversion|\[remote "},
|
|
50
|
+
{"path": "/.git/HEAD", "cat": "vcs", "must": r"ref:\s*refs/"},
|
|
51
|
+
{"path": "/.svn/entries", "cat": "vcs", "must": r"svn|dir\b|\d+\n"},
|
|
52
|
+
{"path": "/.hg/requires", "cat": "vcs", "must": r"revlog|dotencode|store"},
|
|
53
|
+
{"path": "/.env", "cat": "secrets", "must": r"(?m)^[A-Z0-9_]{2,}="},
|
|
54
|
+
{"path": "/.env.local", "cat": "secrets", "must": r"(?m)^[A-Z0-9_]{2,}="},
|
|
55
|
+
{"path": "/.env.production", "cat": "secrets", "must": r"(?m)^[A-Z0-9_]{2,}="},
|
|
56
|
+
{"path": "/.aws/credentials", "cat": "secrets", "must": r"aws_access_key_id|aws_secret_access_key"},
|
|
57
|
+
{"path": "/config.php.bak", "cat": "secrets", "must": r"<\?php|define\(|DB_"},
|
|
58
|
+
{"path": "/wp-config.php.bak", "cat": "secrets", "must": r"DB_PASSWORD|DB_NAME|AUTH_KEY"},
|
|
59
|
+
{"path": "/backup.sql", "cat": "backup", "must": r"INSERT INTO|CREATE TABLE|-- dump|DROP TABLE"},
|
|
60
|
+
{"path": "/dump.sql", "cat": "backup", "must": r"INSERT INTO|CREATE TABLE|-- dump|DROP TABLE"},
|
|
61
|
+
{"path": "/database.sql", "cat": "backup", "must": r"INSERT INTO|CREATE TABLE|-- dump"},
|
|
62
|
+
{"path": "/server-status", "cat": "debug", "must": r"Apache Server Status|Server uptime|Total Traffic"},
|
|
63
|
+
{"path": "/server-info", "cat": "debug", "must": r"Apache Server Information|Server Settings"},
|
|
64
|
+
{"path": "/phpinfo.php", "cat": "debug", "must": r"phpinfo\(\)|PHP Version|php\.ini"},
|
|
65
|
+
{"path": "/info.php", "cat": "debug", "must": r"phpinfo\(\)|PHP Version"},
|
|
66
|
+
{"path": "/actuator", "cat": "debug", "must": r"_links|/actuator"},
|
|
67
|
+
{"path": "/actuator/env", "cat": "debug", "must": r"propertySources|activeProfiles"},
|
|
68
|
+
{"path": "/actuator/heapdump", "cat": "debug", "must": r"JAVA PROFILE|HPROF"},
|
|
69
|
+
{"path": "/swagger-ui.html", "cat": "api-doc", "must": r"Swagger UI|swagger-ui"},
|
|
70
|
+
{"path": "/swagger.json", "cat": "api-doc", "must": r"\"swagger\"|\"openapi\""},
|
|
71
|
+
{"path": "/openapi.json", "cat": "api-doc", "must": r"\"openapi\"|\"swagger\""},
|
|
72
|
+
{"path": "/v2/api-docs", "cat": "api-doc", "must": r"\"swagger\"|\"openapi\""},
|
|
73
|
+
{"path": "/api-docs", "cat": "api-doc", "must": r"\"swagger\"|\"openapi\"|\"paths\""},
|
|
74
|
+
{"path": "/graphql", "cat": "api", "must": r"__schema|\"data\"|\"errors\"|GraphQL"},
|
|
75
|
+
{"path": "/phpmyadmin/", "cat": "admin", "must": r"phpMyAdmin|pma_"},
|
|
76
|
+
{"path": "/adminer.php", "cat": "admin", "must": r"Adminer|Login - Adminer"},
|
|
77
|
+
{"path": "/.gitlab-ci.yml", "cat": "ci", "must": r"(?m)^stages:|^script:|^image:"},
|
|
78
|
+
{"path": "/Jenkinsfile", "cat": "ci", "must": r"pipeline\s*\{|stage\(|node\s*\{"},
|
|
79
|
+
{"path": "/WEB-INF/web.xml", "cat": "framework", "must": r"<web-app|<servlet"},
|
|
80
|
+
{"path": "/.well-known/security.txt", "cat": "hygiene-positive", "must": r"Contact:|Policy:|Expires:"},
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
# Security response headers whose ABSENCE is a posture gap (a neutral FACT, not a vuln).
|
|
84
|
+
SECURITY_HEADERS = {
|
|
85
|
+
"strict-transport-security": "HSTS (transport downgrade protection)",
|
|
86
|
+
"content-security-policy": "CSP (XSS/injection mitigation)",
|
|
87
|
+
"x-frame-options": "clickjacking protection (or frame-ancestors in CSP)",
|
|
88
|
+
"x-content-type-options": "MIME-sniffing protection (nosniff)",
|
|
89
|
+
"referrer-policy": "Referrer-Policy",
|
|
90
|
+
"permissions-policy": "Permissions-Policy",
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _lower_keys(headers: dict) -> dict:
|
|
95
|
+
return {str(k).lower(): str(v) for k, v in (headers or {}).items()}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def header_posture(headers: dict) -> tuple[dict, list[dict]]:
|
|
99
|
+
"""Return (posture, gaps). posture records each security header present/absent (+
|
|
100
|
+
value); gaps is one signal-shaped dict per MISSING header. Pure parsing -- the same
|
|
101
|
+
input always yields the same output, so a benign (well-secured) response reports no
|
|
102
|
+
gap for a header it actually sets (precision)."""
|
|
103
|
+
low = _lower_keys(headers)
|
|
104
|
+
posture, gaps = {}, []
|
|
105
|
+
for h, desc in SECURITY_HEADERS.items():
|
|
106
|
+
present = h in low
|
|
107
|
+
posture[h] = {"present": present, "value": low.get(h, "")}
|
|
108
|
+
if not present:
|
|
109
|
+
gaps.append({"area": "security-header", "title": f"missing {h}", "detail": desc,
|
|
110
|
+
"confidence": "high", "tier_to_confirm": "passive"})
|
|
111
|
+
# CORS posture (fact only): a reflected-Origin + credentials combo is the dangerous one.
|
|
112
|
+
acao = low.get("access-control-allow-origin")
|
|
113
|
+
acac = low.get("access-control-allow-credentials")
|
|
114
|
+
if acao is not None:
|
|
115
|
+
posture["cors"] = {"allow_origin": acao, "allow_credentials": acac or ""}
|
|
116
|
+
return posture, gaps
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def cookie_posture(set_cookie: str) -> list[dict]:
|
|
120
|
+
"""Best-effort flags per Set-Cookie value. urllib may join multiple cookies; we scan
|
|
121
|
+
the raw string for the flags, which is enough for a Phase-0 posture note."""
|
|
122
|
+
out = []
|
|
123
|
+
if not set_cookie:
|
|
124
|
+
return out
|
|
125
|
+
for chunk in set_cookie.split(","):
|
|
126
|
+
name = chunk.strip().split("=", 1)[0].strip()
|
|
127
|
+
if not name or " " in name:
|
|
128
|
+
continue
|
|
129
|
+
low = chunk.lower()
|
|
130
|
+
out.append({"name": name, "secure": "secure" in low, "httponly": "httponly" in low,
|
|
131
|
+
"samesite": ("samesite" in low)})
|
|
132
|
+
return out
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def exposed_sweep(t) -> tuple[list[dict], list[dict]]:
|
|
136
|
+
"""GET each capped path; flag a signal only on 200 AND a content-signature match
|
|
137
|
+
(the precision gate). hygiene-positive hits (security.txt) go to posture, not signals."""
|
|
138
|
+
signals, positives = [], []
|
|
139
|
+
for spec in EXPOSED_PATHS[:MAX_SWEEP_PATHS]:
|
|
140
|
+
r = prod_env._http(t, {"path": spec["path"]})
|
|
141
|
+
if not r.get("ok") or r.get("status") != 200:
|
|
142
|
+
continue
|
|
143
|
+
body = r.get("body") or ""
|
|
144
|
+
if not re.search(spec["must"], body, re.I | re.M):
|
|
145
|
+
continue # 200 but content does not match -> soft-404/decoy; skip (precision)
|
|
146
|
+
if spec["cat"] == "hygiene-positive":
|
|
147
|
+
positives.append({"path": spec["path"], "note": "security.txt present"})
|
|
148
|
+
continue
|
|
149
|
+
signals.append({
|
|
150
|
+
"area": "exposed-path", "category": spec["cat"],
|
|
151
|
+
"title": f"{spec['path']} readable ({spec['cat']})", "path": spec["path"], "status": 200,
|
|
152
|
+
"confidence": "high" if spec["cat"] in ("vcs", "secrets", "backup") else "med",
|
|
153
|
+
"tier_to_confirm": "passive", "snippet": body[:160],
|
|
154
|
+
})
|
|
155
|
+
return signals, positives
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def load_cve_feed(path: str | None = None) -> list[dict]:
|
|
159
|
+
"""Load the version->CVE feed (list of entries). Missing/bad feed => []."""
|
|
160
|
+
p = Path(path or os.environ.get("BLACKGLASS_CVE_FEED", "") or DEFAULT_CVE_FEED)
|
|
161
|
+
try:
|
|
162
|
+
data = json.loads(p.read_text())
|
|
163
|
+
except Exception:
|
|
164
|
+
return []
|
|
165
|
+
return data.get("entries", []) if isinstance(data, dict) else (data or [])
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _vtuple(v) -> tuple:
|
|
169
|
+
"""A dotted version -> tuple of ints ('8.1.2-beta+5' -> (8,1,2)). No digits -> ()."""
|
|
170
|
+
head = re.split(r"[-+ ]", str(v).strip(), 1)[0]
|
|
171
|
+
parts = re.findall(r"\d+", head)
|
|
172
|
+
return tuple(int(x) for x in parts) if parts else ()
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _vcmp(a: tuple, b: tuple) -> int:
|
|
176
|
+
"""Compare version tuples with zero-padding so (1,30) == (1,30,0)."""
|
|
177
|
+
n = max(len(a), len(b))
|
|
178
|
+
a = a + (0,) * (n - len(a))
|
|
179
|
+
b = b + (0,) * (n - len(b))
|
|
180
|
+
return (a > b) - (a < b)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _in_range(v: tuple, rng: dict) -> bool:
|
|
184
|
+
if "ge" in rng and _vcmp(v, _vtuple(rng["ge"])) < 0:
|
|
185
|
+
return False
|
|
186
|
+
if "gt" in rng and _vcmp(v, _vtuple(rng["gt"])) <= 0:
|
|
187
|
+
return False
|
|
188
|
+
if "lt" in rng and _vcmp(v, _vtuple(rng["lt"])) >= 0:
|
|
189
|
+
return False
|
|
190
|
+
if "le" in rng and _vcmp(v, _vtuple(rng["le"])) > 0:
|
|
191
|
+
return False
|
|
192
|
+
return True
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def fingerprint_products(headers: dict) -> list[dict]:
|
|
196
|
+
"""Extract (product, version) pairs from the Server / X-Powered-By banners."""
|
|
197
|
+
low = _lower_keys(headers)
|
|
198
|
+
out, seen = [], set()
|
|
199
|
+
for src in ("server", "x-powered-by"):
|
|
200
|
+
for m in _FP_RE.finditer(low.get(src, "")):
|
|
201
|
+
prod, ver = m.group(1).lower(), m.group(2)
|
|
202
|
+
if (prod, ver) in seen:
|
|
203
|
+
continue
|
|
204
|
+
seen.add((prod, ver))
|
|
205
|
+
out.append({"product": prod, "version": ver, "source": src})
|
|
206
|
+
return out
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def match_cves(products: list[dict], feed: list[dict] | None = None) -> list[dict]:
|
|
210
|
+
"""A SIGNAL (not a verdict) per fingerprinted product whose version falls in a
|
|
211
|
+
known-vulnerable range. A version banner is suggestive only (it can be spoofed or
|
|
212
|
+
backport-patched), so confidence is 'med' and confirmation is a separate tier."""
|
|
213
|
+
feed = feed if feed is not None else load_cve_feed()
|
|
214
|
+
sigs = []
|
|
215
|
+
for p in products:
|
|
216
|
+
pv = _vtuple(p.get("version"))
|
|
217
|
+
if not pv:
|
|
218
|
+
continue
|
|
219
|
+
prod = str(p.get("product", "")).lower()
|
|
220
|
+
for e in feed:
|
|
221
|
+
names = [str(x).lower() for x in (e.get("match") or [e.get("product", "")])]
|
|
222
|
+
if prod not in names:
|
|
223
|
+
continue
|
|
224
|
+
if not any(_in_range(pv, r) for r in (e.get("affected") or [])):
|
|
225
|
+
continue
|
|
226
|
+
sigs.append({
|
|
227
|
+
"area": "known-cve", "product": p.get("product"), "version": p.get("version"),
|
|
228
|
+
"source": p.get("source"), "cve": e.get("id"), "aka": e.get("aka", ""),
|
|
229
|
+
"cvss": e.get("cvss"), "title": e.get("title", ""), "published": e.get("published", ""),
|
|
230
|
+
"confidence": "med", # version-fingerprint match, not a confirmed exploit
|
|
231
|
+
"tier_to_confirm": e.get("confirm", "exploit"),
|
|
232
|
+
"detail": (f"{p.get('product')}/{p.get('version')} is within the affected range. "
|
|
233
|
+
+ e.get("note", "")
|
|
234
|
+
+ " Detection by version fingerprint only; confirmation/exploitation is a separate tiered step."),
|
|
235
|
+
})
|
|
236
|
+
return sigs
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def build_profile(spec: dict, tier: str = "passive") -> dict:
|
|
240
|
+
"""Build the profile by reusing prod_env's passive fetch against the origin only."""
|
|
241
|
+
t = prod_env.Target(spec, [])
|
|
242
|
+
out = {
|
|
243
|
+
"generated_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
|
244
|
+
"tier": tier, "scope": {"origin": t.origin, "root_domain": t.root_domain},
|
|
245
|
+
"target": {}, "posture": {}, "signals": [], "coverage": [],
|
|
246
|
+
"provenance": {"engine": "profile.py", "phase": 0},
|
|
247
|
+
}
|
|
248
|
+
r = prod_env._http(t, {"path": "/", "raw_headers": True})
|
|
249
|
+
if r.get("ok"):
|
|
250
|
+
hdrs = r.get("raw_headers") or r.get("headers") or {}
|
|
251
|
+
low = _lower_keys(hdrs)
|
|
252
|
+
out["target"]["server"] = low.get("server", "")
|
|
253
|
+
out["target"]["x_powered_by"] = low.get("x-powered-by", "")
|
|
254
|
+
out["target"]["status"] = r.get("status")
|
|
255
|
+
posture, gaps = header_posture(hdrs)
|
|
256
|
+
out["posture"]["headers"] = posture
|
|
257
|
+
out["posture"]["cookies"] = cookie_posture(low.get("set-cookie", ""))
|
|
258
|
+
out["signals"].extend(gaps)
|
|
259
|
+
out["coverage"].extend(["security-headers", "cookie-flags", "fingerprint"])
|
|
260
|
+
# Version->CVE track (incl. the notable-CVE feed, e.g. PHP2Shell): fingerprint
|
|
261
|
+
# products from the banners and flag any whose version is in a known range.
|
|
262
|
+
products = fingerprint_products(hdrs)
|
|
263
|
+
if products:
|
|
264
|
+
out["target"]["fingerprints"] = products
|
|
265
|
+
out["signals"].extend(match_cves(products))
|
|
266
|
+
out["coverage"].append("version-cve")
|
|
267
|
+
# Allowed methods (area A): OPTIONS on the root, best-effort (OPTIONS is passive).
|
|
268
|
+
opt = prod_env._http(t, {"path": "/", "method": "OPTIONS", "raw_headers": True})
|
|
269
|
+
allow = _lower_keys(opt.get("raw_headers") or {}).get("allow") if opt.get("ok") else None
|
|
270
|
+
if allow:
|
|
271
|
+
out["target"]["allow_methods"] = allow
|
|
272
|
+
out["coverage"].append("http-methods")
|
|
273
|
+
else:
|
|
274
|
+
out["target"]["error"] = r.get("error")
|
|
275
|
+
# Exposed-path sweep (area C): capped GETs with a content-signature gate for precision.
|
|
276
|
+
sweep_sigs, sweep_pos = exposed_sweep(t)
|
|
277
|
+
out["signals"].extend(sweep_sigs)
|
|
278
|
+
if sweep_pos:
|
|
279
|
+
out["posture"]["hygiene_positives"] = sweep_pos
|
|
280
|
+
out["coverage"].append("exposed-paths")
|
|
281
|
+
https = t.origin.lower().startswith("https")
|
|
282
|
+
out["posture"]["transport"] = {"https": https}
|
|
283
|
+
out["coverage"].append("transport")
|
|
284
|
+
if not https:
|
|
285
|
+
out["signals"].append({"area": "transport", "title": "origin served over plaintext HTTP",
|
|
286
|
+
"detail": "no TLS on the origin", "confidence": "high",
|
|
287
|
+
"tier_to_confirm": "passive"})
|
|
288
|
+
return out
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def main() -> int:
|
|
292
|
+
ap = argparse.ArgumentParser()
|
|
293
|
+
ap.add_argument("--spec", required=True)
|
|
294
|
+
ap.add_argument("--out", default="")
|
|
295
|
+
ap.add_argument("--tier", default="passive")
|
|
296
|
+
a = ap.parse_args()
|
|
297
|
+
spec = json.loads(Path(a.spec).read_text())
|
|
298
|
+
prof = build_profile(spec, a.tier)
|
|
299
|
+
text = json.dumps(prof, indent=2)
|
|
300
|
+
if a.out:
|
|
301
|
+
Path(a.out).write_text(text)
|
|
302
|
+
print(text)
|
|
303
|
+
return 0
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
if __name__ == "__main__":
|
|
307
|
+
raise SystemExit(main())
|