@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.
Files changed (30) hide show
  1. package/README.md +2 -2
  2. package/bin/harness +0 -0
  3. package/package.json +15 -4
  4. package/runners/basic_harness_run.py +1271 -0
  5. package/runners/checklists/anonymous.json +24 -0
  6. package/runners/cve_feed.json +53 -0
  7. package/runners/prod_env.py +1686 -0
  8. package/runners/profile.py +307 -0
  9. package/runners/recon.py +396 -0
  10. package/runners/remediate.py +429 -0
  11. package/runners/report.py +365 -0
  12. package/runners/skills/api/idor-enumeration/SKILL.md +42 -0
  13. package/runners/skills/exploit/authz-differential/SKILL.md +62 -0
  14. package/runners/skills/exploit/business-logic/SKILL.md +53 -0
  15. package/runners/skills/exploit/client-side-exploitation/SKILL.md +63 -0
  16. package/runners/skills/exploit/client-side-rendering/SKILL.md +57 -0
  17. package/runners/skills/exploit/credential-chaining/SKILL.md +56 -0
  18. package/runners/skills/exploit/known-cve-validation/SKILL.md +46 -0
  19. package/runners/skills/exploit/path-traversal/SKILL.md +44 -0
  20. package/runners/skills/exploit/reflected-xss/SKILL.md +43 -0
  21. package/runners/skills/exploit/service-exploitation/SKILL.md +64 -0
  22. package/runners/skills/exploit/service-probing/SKILL.md +51 -0
  23. package/runners/skills/exploit/sqli-inference/SKILL.md +46 -0
  24. package/runners/skills/exploit/ssrf-oob/SKILL.md +48 -0
  25. package/runners/skills/exploit/state-change-exploitation/SKILL.md +72 -0
  26. package/runners/skills/exploit/stateful-authz/SKILL.md +50 -0
  27. package/runners/skills/recon/subdomain-takeover/SKILL.md +45 -0
  28. package/runners/skills/web/content-discovery/SKILL.md +59 -0
  29. package/runners/skills/web/graphql-exposure/SKILL.md +40 -0
  30. 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
+ }