@murk-security/runner-linux-arm64 0.0.0-stage → 0.7.0
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 +1271 -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,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: graphql-exposure
|
|
3
|
+
description: Detect and assess an exposed/over-permissive GraphQL endpoint via introspection, without authentication.
|
|
4
|
+
when-to-use: a /graphql, /api/graphql, /v1/graphql or /query endpoint exists, or a response mentions GraphQL / __schema / Apollo.
|
|
5
|
+
surface: web
|
|
6
|
+
severity: high
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# GraphQL exposure (unauthenticated)
|
|
10
|
+
|
|
11
|
+
A read-only playbook for the `http` op. Goal: show that a GraphQL endpoint leaks
|
|
12
|
+
its schema and/or returns data without authentication.
|
|
13
|
+
|
|
14
|
+
## 1. Locate the endpoint
|
|
15
|
+
Fetch common locations anonymously:
|
|
16
|
+
`http {"path":"/graphql"}`, then `/api/graphql`, `/v1/graphql`, `/query`, `/graphiql`, `/playground`.
|
|
17
|
+
A 200/400 with a JSON body mentioning `errors`/`data`/`__typename` is GraphQL.
|
|
18
|
+
|
|
19
|
+
## 2. Introspection
|
|
20
|
+
Run an introspection query over GET (read-only):
|
|
21
|
+
`http {"path":"/graphql?query={__schema{types{name fields{name}}}}"}`.
|
|
22
|
+
If the response contains `__schema` with type/field names, introspection is ENABLED
|
|
23
|
+
— report it: it hands an attacker the full data model (types, fields, mutations).
|
|
24
|
+
|
|
25
|
+
## 3. Harvest sensitive fields
|
|
26
|
+
From the schema, note fields that should never be anonymously reachable:
|
|
27
|
+
`password`, `passwordHash`, `ssn`, `token`, `secret`, `email`, `role`, `isAdmin`,
|
|
28
|
+
and any `Query`/`Mutation` that reads or changes data without an auth argument.
|
|
29
|
+
|
|
30
|
+
## 4. Prove data exposure
|
|
31
|
+
Issue a narrow query for a sensitive field anonymously, e.g.
|
|
32
|
+
`http {"path":"/graphql?query={users{id email ssn}}"}`.
|
|
33
|
+
A 200 returning real values is a confirmed unauthenticated data exposure — cite the
|
|
34
|
+
exact request + the proving response.
|
|
35
|
+
|
|
36
|
+
## Evidence & severity
|
|
37
|
+
- Introspection enabled alone: medium (information disclosure).
|
|
38
|
+
- Introspection + any anonymous sensitive query returning data: high/critical.
|
|
39
|
+
Report with the endpoint path, the query, and the response fields returned. Do NOT
|
|
40
|
+
send mutations (this is a read-only assessment).
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Skill library for the Blackglass basic-harness product env (Phase 3).
|
|
3
|
+
|
|
4
|
+
Progressive-disclosure skills, in the shape codex + grok-build converged on, but
|
|
5
|
+
adapted to our action model. A skill is:
|
|
6
|
+
|
|
7
|
+
runners/skills/<category>/<name>/SKILL.md
|
|
8
|
+
|
|
9
|
+
with simple `key: value` frontmatter (name, description, when-to-use, surface,
|
|
10
|
+
severity) and a markdown PLAYBOOK body. Unlike codex/grok (which run in a shell
|
|
11
|
+
and can bundle executable scripts), our prober's ONLY action surface is the env
|
|
12
|
+
tool, so a skill carries no executable bundle -- it is pure methodology that tells
|
|
13
|
+
the prober which http/recon ops to call, with which paths and payloads.
|
|
14
|
+
|
|
15
|
+
Three disclosure levels, same as the references:
|
|
16
|
+
1. always-on INDEX (name + when-to-use), budget-capped, injected into the brief
|
|
17
|
+
2. full BODY on demand via the get_skill env op
|
|
18
|
+
3. (n/a for us -- no bundled scripts; the body names the ops to call directly)
|
|
19
|
+
|
|
20
|
+
prod_env.py imports this and exposes list_skills / get_skill ops; build_brief
|
|
21
|
+
injects render_index() as part of the coverage floor.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import os
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
|
|
29
|
+
SKILLS_DIR = Path(os.environ.get("BASIC_HARNESS_SKILLS_DIR", Path(__file__).resolve().parent / "skills"))
|
|
30
|
+
SKILL_FILE = "SKILL.md"
|
|
31
|
+
INDEX_BUDGET = 6000 # max chars for the brief index (budget-cap like codex/grok)
|
|
32
|
+
MAX_DESC_IN_INDEX = 200 # per-line description/when-to-use cap in the index
|
|
33
|
+
MAX_NAME_LEN = 64
|
|
34
|
+
|
|
35
|
+
_CACHE: list[dict] | None = None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _parse_frontmatter(text: str) -> tuple[dict, str]:
|
|
39
|
+
"""Split a SKILL.md into (frontmatter dict, body). Minimal and dependency-free;
|
|
40
|
+
our frontmatter is controlled and uses flat `key: value` lines only."""
|
|
41
|
+
meta: dict = {}
|
|
42
|
+
body = text
|
|
43
|
+
if text.startswith("---"):
|
|
44
|
+
end = text.find("\n---", 3)
|
|
45
|
+
if end != -1:
|
|
46
|
+
fm = text[3:end]
|
|
47
|
+
body = text[end + 4:].lstrip("\n")
|
|
48
|
+
for line in fm.splitlines():
|
|
49
|
+
line = line.strip()
|
|
50
|
+
if not line or line.startswith("#") or ":" not in line:
|
|
51
|
+
continue
|
|
52
|
+
k, _, v = line.partition(":")
|
|
53
|
+
meta[k.strip().lower()] = v.strip().strip('"').strip("'")
|
|
54
|
+
return meta, body
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _normalize_name(name: str) -> str:
|
|
58
|
+
name = "".join(c if (c.isalnum() or c in "-_") else "-" for c in name.lower())
|
|
59
|
+
return name.strip("-")[:MAX_NAME_LEN]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _scan() -> list[dict]:
|
|
63
|
+
global _CACHE
|
|
64
|
+
if _CACHE is not None:
|
|
65
|
+
return _CACHE
|
|
66
|
+
skills: list[dict] = []
|
|
67
|
+
seen: set[str] = set()
|
|
68
|
+
if SKILLS_DIR.is_dir():
|
|
69
|
+
for md in sorted(SKILLS_DIR.rglob(SKILL_FILE)):
|
|
70
|
+
try:
|
|
71
|
+
text = md.read_text(encoding="utf-8")
|
|
72
|
+
except OSError:
|
|
73
|
+
continue
|
|
74
|
+
meta, body = _parse_frontmatter(text)
|
|
75
|
+
name = _normalize_name(meta.get("name") or md.parent.name)
|
|
76
|
+
if not name or name in seen:
|
|
77
|
+
continue
|
|
78
|
+
desc = meta.get("description", "").strip()
|
|
79
|
+
if not desc:
|
|
80
|
+
continue # description is required (matches codex)
|
|
81
|
+
seen.add(name)
|
|
82
|
+
skills.append({
|
|
83
|
+
"name": name,
|
|
84
|
+
"description": desc,
|
|
85
|
+
"when_to_use": (meta.get("when-to-use") or meta.get("when_to_use") or "").strip(),
|
|
86
|
+
"surface": (meta.get("surface") or "").strip(),
|
|
87
|
+
"severity": (meta.get("severity") or "").strip(),
|
|
88
|
+
# The action tier a skill needs to be surfaced. A skill with no
|
|
89
|
+
# declared tier is passive discovery methodology (back-compat: the
|
|
90
|
+
# existing skills). An offensive playbook declares a higher tier so
|
|
91
|
+
# it is never surfaced to a lower-tier (e.g. production passive) run;
|
|
92
|
+
# the caller supplies the `allow` predicate that enforces this.
|
|
93
|
+
"tier": (meta.get("tier") or "").strip(),
|
|
94
|
+
"body": body.strip(),
|
|
95
|
+
"category": md.parent.parent.name if md.parent.parent != SKILLS_DIR else "",
|
|
96
|
+
})
|
|
97
|
+
_CACHE = skills
|
|
98
|
+
return skills
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _visible(s: dict, allow) -> bool:
|
|
102
|
+
"""Whether skill `s` may be surfaced on this run. `allow` is a predicate the
|
|
103
|
+
caller supplies, `allow(skill_tier) -> bool`, which decides from the run's own
|
|
104
|
+
action tier (the env owns the tier vocabulary, not this module). `allow` None
|
|
105
|
+
means no filtering (tests / tier-agnostic callers)."""
|
|
106
|
+
return True if allow is None else bool(allow(s.get("tier", "")))
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _visible_skills(allow) -> list[dict]:
|
|
110
|
+
return [s for s in _scan() if _visible(s, allow)]
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def available(allow=None) -> bool:
|
|
114
|
+
return bool(_visible_skills(allow))
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def render_index(allow=None) -> str:
|
|
118
|
+
"""Budget-capped index for the brief: one line per skill, name + when-to-use
|
|
119
|
+
(or description). Names are always kept; the trigger text is truncated and the
|
|
120
|
+
tail is dropped with an overflow note if the budget is exceeded. Only skills
|
|
121
|
+
the run's tier permits (per `allow`) appear, so an offensive playbook never
|
|
122
|
+
leaks into a lower-tier run's brief."""
|
|
123
|
+
skills = _visible_skills(allow)
|
|
124
|
+
if not skills:
|
|
125
|
+
return ""
|
|
126
|
+
lines, used = [], 0
|
|
127
|
+
for s in skills:
|
|
128
|
+
trigger = s["when_to_use"] or s["description"]
|
|
129
|
+
if len(trigger) > MAX_DESC_IN_INDEX:
|
|
130
|
+
trigger = trigger[:MAX_DESC_IN_INDEX - 1].rstrip() + "…"
|
|
131
|
+
line = f"- {s['name']}: {trigger}"
|
|
132
|
+
if used + len(line) + 1 > INDEX_BUDGET:
|
|
133
|
+
lines.append(f"- … and {len(skills) - len(lines)} more (call list_skills)")
|
|
134
|
+
break
|
|
135
|
+
lines.append(line)
|
|
136
|
+
used += len(line) + 1
|
|
137
|
+
return "\n".join(lines)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def list_skills(allow=None) -> dict:
|
|
141
|
+
return {"ok": True, "skills": [
|
|
142
|
+
{k: s[k] for k in ("name", "description", "when_to_use", "surface", "severity", "category")}
|
|
143
|
+
for s in _visible_skills(allow)]}
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def get_skill(name: str, allow=None) -> dict:
|
|
147
|
+
want = _normalize_name(str(name))
|
|
148
|
+
for s in _scan():
|
|
149
|
+
if s["name"] == want:
|
|
150
|
+
if not _visible(s, allow):
|
|
151
|
+
# Defense in depth: a skill the run's tier does not permit cannot be
|
|
152
|
+
# loaded even by naming it directly (mirrors the env refusing a
|
|
153
|
+
# higher-tier op). Don't return the body or methodology.
|
|
154
|
+
return {"ok": False, "error": f"skill {s['name']!r} is not available at this action tier"}
|
|
155
|
+
return {"ok": True, "name": s["name"], "surface": s["surface"],
|
|
156
|
+
"severity": s["severity"], "body": s["body"]}
|
|
157
|
+
names = ", ".join(s["name"] for s in _visible_skills(allow)) or "(none)"
|
|
158
|
+
return {"ok": False, "error": f"no skill {name!r}; available: {names}"}
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def handle(op: str, args: dict, allow=None):
|
|
162
|
+
"""Return a result dict for a skill op, or None if op is not a skill op."""
|
|
163
|
+
if op == "list_skills":
|
|
164
|
+
return list_skills(allow)
|
|
165
|
+
if op == "get_skill":
|
|
166
|
+
return get_skill(args.get("name", ""), allow)
|
|
167
|
+
return None
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def describe_ops() -> dict:
|
|
171
|
+
return {
|
|
172
|
+
"list_skills": "{} list the deep playbooks you can load (name + when-to-use).",
|
|
173
|
+
"get_skill": '{"name": "..."} load a playbook\'s full methodology (which ops/paths/payloads to use).',
|
|
174
|
+
}
|