@intentsolutionsio/penetration-tester 2.0.0 → 3.0.4

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 (112) hide show
  1. package/.claude-plugin/plugin.json +8 -3
  2. package/README.md +8 -0
  3. package/commands/pentest.md +5 -0
  4. package/package.json +8 -3
  5. package/skills/analyzing-tls-config/SKILL.md +221 -0
  6. package/skills/analyzing-tls-config/references/AUTHORIZATION.md +133 -0
  7. package/skills/analyzing-tls-config/references/PLAYBOOK.md +267 -0
  8. package/skills/analyzing-tls-config/references/THEORY.md +128 -0
  9. package/skills/analyzing-tls-config/scripts/analyze_tls.py +415 -0
  10. package/skills/auditing-cors-policy/SKILL.md +186 -0
  11. package/skills/auditing-cors-policy/references/PLAYBOOK.md +220 -0
  12. package/skills/auditing-cors-policy/references/THEORY.md +142 -0
  13. package/skills/auditing-cors-policy/scripts/audit_cors.py +350 -0
  14. package/skills/auditing-npm-dependencies/SKILL.md +254 -0
  15. package/skills/auditing-npm-dependencies/references/PLAYBOOK.md +175 -0
  16. package/skills/auditing-npm-dependencies/references/THEORY.md +122 -0
  17. package/skills/auditing-npm-dependencies/scripts/audit_npm.py +408 -0
  18. package/skills/auditing-python-dependencies/SKILL.md +251 -0
  19. package/skills/auditing-python-dependencies/references/PLAYBOOK.md +193 -0
  20. package/skills/auditing-python-dependencies/references/THEORY.md +122 -0
  21. package/skills/auditing-python-dependencies/scripts/audit_python.py +459 -0
  22. package/skills/checking-http-security-headers/SKILL.md +176 -0
  23. package/skills/checking-http-security-headers/references/PLAYBOOK.md +212 -0
  24. package/skills/checking-http-security-headers/references/THEORY.md +137 -0
  25. package/skills/checking-http-security-headers/scripts/check_headers.py +362 -0
  26. package/skills/checking-license-compliance/SKILL.md +225 -0
  27. package/skills/checking-license-compliance/references/PLAYBOOK.md +161 -0
  28. package/skills/checking-license-compliance/references/THEORY.md +152 -0
  29. package/skills/checking-license-compliance/scripts/check_licenses.py +461 -0
  30. package/skills/composing-vulnerability-report/SKILL.md +212 -0
  31. package/skills/composing-vulnerability-report/references/PLAYBOOK.md +180 -0
  32. package/skills/composing-vulnerability-report/references/THEORY.md +178 -0
  33. package/skills/composing-vulnerability-report/scripts/compose_report.py +396 -0
  34. package/skills/confirming-pentest-authorization/SKILL.md +247 -0
  35. package/skills/confirming-pentest-authorization/references/PLAYBOOK.md +189 -0
  36. package/skills/confirming-pentest-authorization/references/THEORY.md +167 -0
  37. package/skills/confirming-pentest-authorization/scripts/check_authorization.py +457 -0
  38. package/skills/defining-pentest-scope/SKILL.md +227 -0
  39. package/skills/defining-pentest-scope/references/PLAYBOOK.md +238 -0
  40. package/skills/defining-pentest-scope/references/THEORY.md +170 -0
  41. package/skills/defining-pentest-scope/scripts/define_scope.py +472 -0
  42. package/skills/detecting-command-injection-patterns/SKILL.md +144 -0
  43. package/skills/detecting-command-injection-patterns/references/PLAYBOOK.md +302 -0
  44. package/skills/detecting-command-injection-patterns/references/THEORY.md +206 -0
  45. package/skills/detecting-command-injection-patterns/scripts/scan_cmdi.py +290 -0
  46. package/skills/detecting-debug-endpoints/SKILL.md +207 -0
  47. package/skills/detecting-debug-endpoints/references/PLAYBOOK.md +402 -0
  48. package/skills/detecting-debug-endpoints/references/THEORY.md +218 -0
  49. package/skills/detecting-debug-endpoints/scripts/probe_debug.py +518 -0
  50. package/skills/detecting-directory-listing/SKILL.md +206 -0
  51. package/skills/detecting-directory-listing/references/PLAYBOOK.md +277 -0
  52. package/skills/detecting-directory-listing/references/THEORY.md +203 -0
  53. package/skills/detecting-directory-listing/scripts/probe_directory_listing.py +180 -0
  54. package/skills/detecting-eval-exec-usage/SKILL.md +128 -0
  55. package/skills/detecting-eval-exec-usage/references/PLAYBOOK.md +306 -0
  56. package/skills/detecting-eval-exec-usage/references/THEORY.md +159 -0
  57. package/skills/detecting-eval-exec-usage/scripts/scan_eval.py +223 -0
  58. package/skills/detecting-exposed-secrets-files/SKILL.md +179 -0
  59. package/skills/detecting-exposed-secrets-files/references/PLAYBOOK.md +274 -0
  60. package/skills/detecting-exposed-secrets-files/references/THEORY.md +174 -0
  61. package/skills/detecting-exposed-secrets-files/scripts/probe_secrets.py +207 -0
  62. package/skills/detecting-insecure-deserialization/SKILL.md +148 -0
  63. package/skills/detecting-insecure-deserialization/references/PLAYBOOK.md +333 -0
  64. package/skills/detecting-insecure-deserialization/references/THEORY.md +199 -0
  65. package/skills/detecting-insecure-deserialization/scripts/scan_deserialization.py +250 -0
  66. package/skills/detecting-sql-injection-patterns/SKILL.md +161 -0
  67. package/skills/detecting-sql-injection-patterns/references/PLAYBOOK.md +317 -0
  68. package/skills/detecting-sql-injection-patterns/references/THEORY.md +261 -0
  69. package/skills/detecting-sql-injection-patterns/scripts/scan_sqli.py +354 -0
  70. package/skills/detecting-ssl-cert-issues/SKILL.md +182 -0
  71. package/skills/detecting-ssl-cert-issues/references/PLAYBOOK.md +203 -0
  72. package/skills/detecting-ssl-cert-issues/references/THEORY.md +133 -0
  73. package/skills/detecting-ssl-cert-issues/scripts/check_cert_chain.py +481 -0
  74. package/skills/detecting-weak-cryptography/SKILL.md +147 -0
  75. package/skills/detecting-weak-cryptography/references/PLAYBOOK.md +466 -0
  76. package/skills/detecting-weak-cryptography/references/THEORY.md +194 -0
  77. package/skills/detecting-weak-cryptography/scripts/scan_weak_crypto.py +417 -0
  78. package/skills/fingerprinting-server-software/SKILL.md +191 -0
  79. package/skills/fingerprinting-server-software/references/PLAYBOOK.md +337 -0
  80. package/skills/fingerprinting-server-software/references/THEORY.md +183 -0
  81. package/skills/fingerprinting-server-software/scripts/fingerprint_server.py +347 -0
  82. package/skills/generating-executive-summary/SKILL.md +261 -0
  83. package/skills/generating-executive-summary/references/PLAYBOOK.md +201 -0
  84. package/skills/generating-executive-summary/references/THEORY.md +195 -0
  85. package/skills/generating-executive-summary/scripts/exec_summary.py +538 -0
  86. package/skills/mapping-findings-to-owasp-top10/SKILL.md +235 -0
  87. package/skills/mapping-findings-to-owasp-top10/references/PLAYBOOK.md +193 -0
  88. package/skills/mapping-findings-to-owasp-top10/references/THEORY.md +160 -0
  89. package/skills/mapping-findings-to-owasp-top10/scripts/map_owasp.py +540 -0
  90. package/skills/performing-penetration-testing/SKILL.md +282 -190
  91. package/skills/performing-penetration-testing/references/OWASP_TOP_10.md +22 -0
  92. package/skills/performing-penetration-testing/references/REMEDIATION_PLAYBOOK.md +46 -0
  93. package/skills/performing-penetration-testing/references/SECURITY_HEADERS.md +41 -0
  94. package/skills/performing-penetration-testing/scripts/code_security_scanner.py +144 -79
  95. package/skills/performing-penetration-testing/scripts/dependency_auditor.py +116 -93
  96. package/skills/performing-penetration-testing/scripts/security_scanner.py +574 -446
  97. package/skills/probing-dangerous-http-methods/SKILL.md +182 -0
  98. package/skills/probing-dangerous-http-methods/references/PLAYBOOK.md +234 -0
  99. package/skills/probing-dangerous-http-methods/references/THEORY.md +145 -0
  100. package/skills/probing-dangerous-http-methods/scripts/probe_methods.py +263 -0
  101. package/skills/recording-pentest-engagement/SKILL.md +253 -0
  102. package/skills/recording-pentest-engagement/references/PLAYBOOK.md +203 -0
  103. package/skills/recording-pentest-engagement/references/THEORY.md +195 -0
  104. package/skills/recording-pentest-engagement/scripts/record_engagement.py +461 -0
  105. package/skills/scanning-for-hardcoded-secrets/SKILL.md +215 -0
  106. package/skills/scanning-for-hardcoded-secrets/references/PLAYBOOK.md +325 -0
  107. package/skills/scanning-for-hardcoded-secrets/references/THEORY.md +175 -0
  108. package/skills/scanning-for-hardcoded-secrets/scripts/scan_secrets.py +395 -0
  109. package/skills/tracing-transitive-vulnerabilities/SKILL.md +235 -0
  110. package/skills/tracing-transitive-vulnerabilities/references/PLAYBOOK.md +233 -0
  111. package/skills/tracing-transitive-vulnerabilities/references/THEORY.md +138 -0
  112. package/skills/tracing-transitive-vulnerabilities/scripts/trace_vulns.py +484 -0
@@ -0,0 +1,457 @@
1
+ #!/usr/bin/env python3
2
+ """confirming-pentest-authorization — verify ROE before any scan runs.
3
+
4
+ Reads a Rules-of-Engagement YAML attestation, validates required fields
5
+ (authorizer, in_scope_targets, time_window, emergency_contact, signature_block),
6
+ checks signer identity against an allowed-authorizers file (optional), and
7
+ verifies the current time falls within the engagement window. Emits Findings
8
+ via lib/finding.py. The orchestrator routes here FIRST — any CRITICAL finding
9
+ halts engagement before later cluster skills get a chance to run.
10
+
11
+ Usage:
12
+ python3 check_authorization.py [--roe FILE] [--allowed FILE]
13
+ [--check-target HOST]
14
+ [--output FILE] [--format json|jsonl|markdown]
15
+ [--min-severity sev]
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import argparse
21
+ import datetime as dt
22
+ import ipaddress
23
+ import sys
24
+ from pathlib import Path
25
+ from typing import Any
26
+
27
+ # --- lib/ import -------------------------------------------------------------
28
+ _LIB_ROOT = Path(__file__).resolve().parents[3]
29
+ sys.path.insert(0, str(_LIB_ROOT))
30
+
31
+ from lib.finding import Finding, Severity # noqa: E402
32
+ from lib import report # noqa: E402
33
+
34
+ # --- Optional YAML import (fallback to a minimal parser if PyYAML absent) ---
35
+ try:
36
+ import yaml # type: ignore[import-not-found]
37
+
38
+ _HAS_PYYAML = True
39
+ except ImportError: # pragma: no cover
40
+ yaml = None
41
+ _HAS_PYYAML = False
42
+
43
+
44
+ SKILL_ID = "confirming-pentest-authorization"
45
+ CATEGORY = "engagement-authorization"
46
+
47
+ REQUIRED_FIELDS = (
48
+ "engagement_id",
49
+ "authorizer",
50
+ "in_scope_targets",
51
+ "time_window",
52
+ "emergency_contact",
53
+ "signature_block",
54
+ )
55
+ REQUIRED_AUTHORIZER_FIELDS = ("name", "email", "role")
56
+ REQUIRED_TIME_WINDOW_FIELDS = ("start", "end")
57
+ REQUIRED_SIGNATURE_FIELDS = ("signer", "signed_at", "signature")
58
+
59
+
60
+ # --- Minimal-YAML fallback parser -------------------------------------------
61
+
62
+
63
+ def _minimal_yaml_load(text: str) -> dict[str, Any]:
64
+ """Very limited YAML parser for environments without PyYAML.
65
+
66
+ Supports: top-level mapping, nested mappings (indentation-sensitive),
67
+ lists of dicts and scalars, ISO date/time strings as strings.
68
+ Refuses any structure it can't parse rather than guess.
69
+ """
70
+ if _HAS_PYYAML:
71
+ return yaml.safe_load(text) or {}
72
+
73
+ lines = [l for l in text.splitlines() if not l.lstrip().startswith("#")]
74
+ root: dict[str, Any] = {}
75
+ stack: list[tuple[int, Any]] = [(-1, root)]
76
+
77
+ def cur() -> Any:
78
+ return stack[-1][1]
79
+
80
+ for raw in lines:
81
+ if not raw.strip():
82
+ continue
83
+ indent = len(raw) - len(raw.lstrip())
84
+ line = raw.strip()
85
+ while stack and stack[-1][0] >= indent:
86
+ stack.pop()
87
+
88
+ if line.startswith("- "):
89
+ item_text = line[2:].strip()
90
+ container = cur()
91
+ if not isinstance(container, list):
92
+ # parent expected a list
93
+ parent_key, parent_dict = stack[-2] if len(stack) >= 2 else (-1, root)
94
+ # find the key for which we want to make a list — this is best-effort
95
+ container = []
96
+ # re-attach: convert empty value of last set key to a list
97
+ if isinstance(parent_dict, dict):
98
+ for k in list(parent_dict.keys())[::-1]:
99
+ if parent_dict[k] in (None, "", {}):
100
+ parent_dict[k] = container
101
+ break
102
+ if ":" in item_text and not item_text.endswith(":"):
103
+ k, _, v = item_text.partition(":")
104
+ container.append({k.strip(): v.strip()})
105
+ stack.append((indent + 2, container[-1]))
106
+ elif item_text.endswith(":"):
107
+ obj: dict[str, Any] = {}
108
+ container.append(obj)
109
+ stack.append((indent + 2, obj))
110
+ else:
111
+ container.append(item_text)
112
+ elif ":" in line:
113
+ key, _, value = line.partition(":")
114
+ key = key.strip()
115
+ value = value.strip()
116
+ container = cur()
117
+ if not isinstance(container, dict):
118
+ continue
119
+ if value == "":
120
+ container[key] = {}
121
+ stack.append((indent, container[key]))
122
+ elif value == "|":
123
+ # block scalar — collect indented lines (not implemented richly)
124
+ container[key] = ""
125
+ else:
126
+ container[key] = value.strip().strip('"').strip("'")
127
+ return root
128
+
129
+
130
+ # --- ROE loading ------------------------------------------------------------
131
+
132
+
133
+ def load_roe(path: Path) -> tuple[dict[str, Any] | None, str | None]:
134
+ if not path.exists():
135
+ return None, f"ROE file not found at {path}"
136
+ try:
137
+ text = path.read_text(encoding="utf-8")
138
+ except OSError as e:
139
+ return None, f"Cannot read {path}: {e}"
140
+ try:
141
+ data = _minimal_yaml_load(text) if not _HAS_PYYAML else yaml.safe_load(text)
142
+ except Exception as e: # noqa: BLE001 — YAML errors come in many flavors
143
+ return None, f"YAML parse error: {e}"
144
+ if not isinstance(data, dict):
145
+ return None, "ROE top-level structure is not a mapping"
146
+ return data, None
147
+
148
+
149
+ def load_allowed(path: Path) -> list[str]:
150
+ if not path.exists():
151
+ return []
152
+ return [
153
+ line.strip()
154
+ for line in path.read_text(encoding="utf-8").splitlines()
155
+ if line.strip() and not line.startswith("#")
156
+ ]
157
+
158
+
159
+ # --- Field validation -------------------------------------------------------
160
+
161
+
162
+ def _info(title: str, target: str, detail: str) -> Finding:
163
+ return Finding(
164
+ skill_id=SKILL_ID,
165
+ title=title,
166
+ severity=Severity.INFO,
167
+ target=target,
168
+ detail=detail,
169
+ remediation="No action required.",
170
+ )
171
+
172
+
173
+ def _critical(title: str, target: str, detail: str, remediation: str) -> Finding:
174
+ return Finding(
175
+ skill_id=SKILL_ID,
176
+ title=title,
177
+ severity=Severity.CRITICAL,
178
+ target=target,
179
+ detail=detail,
180
+ remediation=remediation,
181
+ )
182
+
183
+
184
+ def _high(title: str, target: str, detail: str, remediation: str) -> Finding:
185
+ return Finding(
186
+ skill_id=SKILL_ID,
187
+ title=title,
188
+ severity=Severity.HIGH,
189
+ target=target,
190
+ detail=detail,
191
+ remediation=remediation,
192
+ )
193
+
194
+
195
+ def _parse_iso(s: str) -> dt.datetime | None:
196
+ try:
197
+ # accept "Z" suffix and offset forms
198
+ s2 = s.replace("Z", "+00:00")
199
+ return dt.datetime.fromisoformat(s2)
200
+ except (ValueError, TypeError):
201
+ return None
202
+
203
+
204
+ def validate_roe(data: dict[str, Any], target_label: str, allowed: list[str]) -> list[Finding]:
205
+ findings: list[Finding] = []
206
+
207
+ # Required top-level fields
208
+ for field in REQUIRED_FIELDS:
209
+ if field not in data or data.get(field) in (None, "", [], {}):
210
+ findings.append(
211
+ _critical(
212
+ f"ROE missing required field: {field}",
213
+ target_label,
214
+ f"Field `{field}` is required and was missing or empty.",
215
+ f"Add `{field}` to the ROE and re-sign.",
216
+ )
217
+ )
218
+
219
+ authorizer = data.get("authorizer") or {}
220
+ if isinstance(authorizer, dict):
221
+ for sub in REQUIRED_AUTHORIZER_FIELDS:
222
+ if sub not in authorizer or not authorizer.get(sub):
223
+ findings.append(
224
+ _critical(
225
+ f"authorizer.{sub} missing",
226
+ target_label,
227
+ f"The authorizer block is missing `{sub}`.",
228
+ f"Add `authorizer.{sub}` to the ROE and re-sign.",
229
+ )
230
+ )
231
+
232
+ tw = data.get("time_window") or {}
233
+ if isinstance(tw, dict):
234
+ for sub in REQUIRED_TIME_WINDOW_FIELDS:
235
+ if sub not in tw:
236
+ findings.append(
237
+ _critical(
238
+ f"time_window.{sub} missing",
239
+ target_label,
240
+ f"`time_window.{sub}` is required.",
241
+ f"Add `time_window.{sub}` (ISO-8601 timestamp) and re-sign.",
242
+ )
243
+ )
244
+ start = _parse_iso(str(tw.get("start", "")))
245
+ end = _parse_iso(str(tw.get("end", "")))
246
+ now = dt.datetime.now(dt.timezone.utc)
247
+ if start and now < start:
248
+ findings.append(
249
+ _high(
250
+ "engagement time window has not started",
251
+ target_label,
252
+ f"Current time {now.isoformat()} precedes window start {start.isoformat()}.",
253
+ "Wait until the start time, or have the authorizer re-sign with an earlier start.",
254
+ )
255
+ )
256
+ if end and now > end:
257
+ findings.append(
258
+ _high(
259
+ "engagement time window has expired",
260
+ target_label,
261
+ f"Current time {now.isoformat()} is past window end {end.isoformat()}.",
262
+ "Halt testing immediately. Request a new ROE with an extended end date.",
263
+ )
264
+ )
265
+
266
+ sig = data.get("signature_block") or {}
267
+ if isinstance(sig, dict):
268
+ for sub in REQUIRED_SIGNATURE_FIELDS:
269
+ if sub not in sig or not sig.get(sub):
270
+ findings.append(
271
+ _critical(
272
+ f"signature_block.{sub} missing",
273
+ target_label,
274
+ f"The signature_block is missing `{sub}`.",
275
+ "Re-sign the ROE with all signature fields present.",
276
+ )
277
+ )
278
+ signer = str(sig.get("signer", "")).strip().lower()
279
+ if allowed and signer and signer not in [a.lower() for a in allowed]:
280
+ findings.append(
281
+ _critical(
282
+ "signer not in allowed-authorizers list",
283
+ target_label,
284
+ f"signer `{signer}` is not present in the allowed-authorizers "
285
+ f"file. Engagement cannot be authorized by this signer.",
286
+ "Either add the signer to the allowed list (only do this "
287
+ "for verified authorizers!) or have an allowed signer "
288
+ "re-sign the ROE.",
289
+ )
290
+ )
291
+
292
+ signed_at = _parse_iso(str(sig.get("signed_at", "")))
293
+ if signed_at:
294
+ age_days = (dt.datetime.now(dt.timezone.utc) - signed_at).days
295
+ if age_days > 30:
296
+ findings.append(
297
+ Finding(
298
+ skill_id=SKILL_ID,
299
+ title="ROE is stale (>30 days since signature)",
300
+ severity=Severity.MEDIUM,
301
+ target=target_label,
302
+ detail=f"ROE was signed {age_days} days ago.",
303
+ remediation="Request a refreshed ROE signature.",
304
+ )
305
+ )
306
+
307
+ in_scope = data.get("in_scope_targets") or []
308
+ if isinstance(in_scope, list) and len(in_scope) == 0:
309
+ findings.append(
310
+ _high(
311
+ "in_scope_targets list is empty",
312
+ target_label,
313
+ "in_scope_targets must list at least one target.",
314
+ "Add explicit in-scope targets and re-sign.",
315
+ )
316
+ )
317
+
318
+ return findings
319
+
320
+
321
+ # --- Target-in-scope check --------------------------------------------------
322
+
323
+
324
+ def _normalize_targets(in_scope: list[dict[str, Any] | str]) -> list[str]:
325
+ norm: list[str] = []
326
+ for t in in_scope:
327
+ if isinstance(t, dict):
328
+ if "host" in t:
329
+ norm.append(str(t["host"]).strip().lower())
330
+ elif "cidr" in t:
331
+ norm.append(str(t["cidr"]).strip())
332
+ elif isinstance(t, str):
333
+ norm.append(t.strip().lower())
334
+ return norm
335
+
336
+
337
+ def target_in_scope(target: str, in_scope: list[str]) -> bool:
338
+ target_n = target.strip().lower()
339
+ # exact hostname match
340
+ if target_n in in_scope:
341
+ return True
342
+ # CIDR membership
343
+ try:
344
+ target_ip = ipaddress.ip_address(target_n)
345
+ for entry in in_scope:
346
+ try:
347
+ net = ipaddress.ip_network(entry, strict=False)
348
+ if target_ip in net:
349
+ return True
350
+ except (ValueError, TypeError):
351
+ continue
352
+ except ValueError:
353
+ pass
354
+ # Suffix match for subdomain wildcards (e.g. "*.acme.example")
355
+ for entry in in_scope:
356
+ if entry.startswith("*.") and target_n.endswith(entry[2:]):
357
+ return True
358
+ return False
359
+
360
+
361
+ def check_targets(targets: list[str], in_scope_raw: list[dict[str, Any] | str], target_label: str) -> list[Finding]:
362
+ in_scope = _normalize_targets(in_scope_raw)
363
+ out: list[Finding] = []
364
+ for t in targets:
365
+ if target_in_scope(t, in_scope):
366
+ out.append(
367
+ _info(
368
+ f"target {t} is in scope",
369
+ target_label,
370
+ f"`{t}` matched an in-scope entry in the ROE.",
371
+ )
372
+ )
373
+ else:
374
+ out.append(
375
+ _critical(
376
+ f"target {t} is NOT in scope",
377
+ target_label,
378
+ f"`{t}` does not match any in-scope entry. Probing this target is not authorized.",
379
+ f"Either confirm the target IS in scope (request an ROE "
380
+ f"amendment) or remove `{t}` from the test plan.",
381
+ )
382
+ )
383
+ return out
384
+
385
+
386
+ # --- CLI ---------------------------------------------------------------------
387
+
388
+
389
+ def _build_arg_parser() -> argparse.ArgumentParser:
390
+ p = argparse.ArgumentParser(description=__doc__.split("\n")[0])
391
+ p.add_argument("--roe", default="roe.yaml", help="Path to ROE YAML file")
392
+ p.add_argument("--allowed", default=".allowed-authorizers")
393
+ p.add_argument(
394
+ "--check-target",
395
+ action="append",
396
+ default=[],
397
+ help="Verify target is in-scope (repeatable)",
398
+ )
399
+ p.add_argument("--output", default=None)
400
+ p.add_argument("--format", default="markdown", choices=["json", "jsonl", "markdown"])
401
+ p.add_argument(
402
+ "--min-severity",
403
+ default="info",
404
+ choices=["info", "low", "medium", "high", "critical"],
405
+ )
406
+ return p
407
+
408
+
409
+ def _filter_min_severity(findings: list[Finding], min_sev: str) -> list[Finding]:
410
+ floor = Severity(min_sev).numeric
411
+ return [f for f in findings if f.severity.numeric >= floor]
412
+
413
+
414
+ def main(argv: list[str] | None = None) -> int:
415
+ args = _build_arg_parser().parse_args(argv)
416
+ roe_path = Path(args.roe).resolve()
417
+ allowed_path = Path(args.allowed).resolve()
418
+
419
+ data, err = load_roe(roe_path)
420
+ if data is None:
421
+ f = _critical(
422
+ "ROE could not be loaded",
423
+ str(roe_path),
424
+ err or "unknown error",
425
+ f"Create or fix the ROE at {roe_path} and re-run.",
426
+ )
427
+ report.emit([f], args.output, args.format, scan_target=str(roe_path))
428
+ return 1
429
+
430
+ allowed = load_allowed(allowed_path)
431
+ findings = validate_roe(data, str(roe_path), allowed)
432
+
433
+ if args.check_target:
434
+ findings.extend(
435
+ check_targets(
436
+ args.check_target,
437
+ data.get("in_scope_targets") or [],
438
+ str(roe_path),
439
+ )
440
+ )
441
+
442
+ if not findings:
443
+ findings = [
444
+ _info(
445
+ "engagement is authorized",
446
+ str(roe_path),
447
+ "All required fields present, signer in allowlist, time window "
448
+ "is active, and all checked targets are in scope.",
449
+ )
450
+ ]
451
+ findings = _filter_min_severity(findings, args.min_severity)
452
+ report.emit(findings, args.output, args.format, scan_target=str(roe_path))
453
+ return report.exit_code(findings)
454
+
455
+
456
+ if __name__ == "__main__":
457
+ sys.exit(main())
@@ -0,0 +1,227 @@
1
+ ---
2
+ name: defining-pentest-scope
3
+ description: |
4
+ Parse the ROE scope definition, enumerate every in-scope target
5
+ (hostnames, IPs, CIDRs, URLs, cloud accounts, SaaS tenants),
6
+ validate syntax, detect overlap with out-of-scope or known
7
+ third-party SaaS ranges, and emit a normalized target list plus
8
+ IP allowlist for scanning tools. Runs after confirming-pentest-
9
+ authorization and before any cluster 1-4 scan.
10
+ Use when: starting an engagement, expanding scope mid-engagement,
11
+ validating that a target list matches the ROE, or generating an
12
+ allowlist for an external scanner.
13
+ Threshold: malformed syntax, in-scope overlap with out-of-scope,
14
+ reserved or third-party SaaS ranges without acknowledgement.
15
+ Trigger with: "define scope", "enumerate targets", "validate
16
+ target list", "generate IP allowlist".
17
+ allowed-tools:
18
+ - Read
19
+ - Bash(python3:*)
20
+ - Glob
21
+ disallowed-tools:
22
+ - Bash(rm:*)
23
+ - Bash(curl:*)
24
+ - Bash(wget:*)
25
+ - Bash(nmap:*)
26
+ - Write(.env)
27
+ - Edit(.env)
28
+ version: 3.0.0-dev
29
+ author: Jeremy Longshore <jeremy@intentsolutions.io>
30
+ license: MIT
31
+ compatibility: Designed for Claude Code
32
+ tags:
33
+ - security
34
+ - engagement-governance
35
+ - scope
36
+ - target-enumeration
37
+ - pentest
38
+ ---
39
+
40
+ # Defining Pentest Scope
41
+
42
+ ## Overview
43
+
44
+ A pentest scope is a list of permission boundaries. Get it wrong
45
+ and you either (a) miss real exposure by failing to test something
46
+ the customer expected covered, or (b) probe something you weren't
47
+ allowed to touch and turn the engagement into a liability event.
48
+ Both failure modes share a root cause: the scope list was a vague
49
+ narrative ("test the marketing site and the API") rather than a
50
+ machine-readable, syntactically-validated, conflict-checked
51
+ artifact.
52
+
53
+ This skill takes the in-scope and out-of-scope sections from a ROE
54
+ and produces three deliverables:
55
+
56
+ 1. **Normalized target list** — every entry parsed into a
57
+ structured form (host vs CIDR vs URL path vs cloud account vs
58
+ SaaS tenant), with explicit type tagging. Downstream cluster
59
+ 1-4 skills consume this list rather than raw strings.
60
+ 2. **IP allowlist** — flat list of IPv4 and IPv6 addresses /
61
+ CIDRs ready to paste into scanner configurations (nmap target
62
+ list, Burp scope file, AWS WAF allowlist, etc.).
63
+ 3. **Conflict report** — Findings flagging syntactically-malformed
64
+ entries, overlap between in-scope and out-of-scope, inclusion
65
+ of reserved ranges (RFC1918, link-local, multicast), and known
66
+ third-party SaaS infrastructure that needs separate authz.
67
+
68
+ The skill does NOT perform DNS resolution or network probing —
69
+ that would itself be a "first probe" of the target, which by the
70
+ governance model must happen AFTER scope is locked.
71
+
72
+ ## When the skill produces findings
73
+
74
+ | Finding | Severity | Threshold | Affected control |
75
+ |---|---|---|---|
76
+ | Malformed target syntax | **HIGH** | Entry doesn't parse as host / CIDR / URL / account-id | (legal) |
77
+ | In-scope overlaps out-of-scope | **CRITICAL** | An in-scope target falls within an out-of-scope CIDR | (legal) |
78
+ | Reserved range without acknowledgement | **HIGH** | RFC1918, link-local (169.254/16), multicast (224/4), broadcast in in-scope list | (operational) |
79
+ | Known third-party SaaS in scope | **HIGH** | In-scope IP matches a known SaaS range (AWS, Cloudflare, GitHub, etc.) without separate authz | (legal) |
80
+ | Duplicate target | **INFO** | Same target appears multiple times | (operational) |
81
+ | Wildcard subdomain (e.g. `*.acme.example`) | **INFO** | Wildcards expand at scan time | (informational) |
82
+ | All targets validated cleanly | **INFO** | Positive confirmation | (informational) |
83
+
84
+ ## Prerequisites
85
+
86
+ - Python 3.9+
87
+ - ROE file at `./roe.yaml` (or pass `--roe FILE`)
88
+ - Optional `.scope-extension.yaml` listing additional targets
89
+ added mid-engagement (each must reference an authz amendment)
90
+
91
+ ## Target syntax forms
92
+
93
+ | Form | Example | Notes |
94
+ |---|---|---|
95
+ | Hostname | `app.acme.example` | DNS-resolvable name |
96
+ | Wildcard subdomain | `*.acme.example` | Resolved at scan time; flag for explicit acknowledgement |
97
+ | IPv4 address | `203.0.113.10` | Single host |
98
+ | IPv4 CIDR | `203.0.113.0/24` | Network range |
99
+ | IPv6 address | `2001:db8::10` | Single host |
100
+ | IPv6 CIDR | `2001:db8::/32` | Network range |
101
+ | URL with path | `https://app.acme.example/api/v2` | Path-restricted scope |
102
+ | Cloud account ID | `aws:123456789012` or `gcp:acme-prod` | Cloud control-plane scope |
103
+ | SaaS tenant | `okta:acme-corp` or `auth0:acme` | SaaS-tenant scope |
104
+
105
+ ## Instructions
106
+
107
+ ### Step 1 — Provide the scope source
108
+
109
+ The skill reads the ROE's `in_scope_targets` and
110
+ `out_of_scope_targets` sections by default. Override with
111
+ `--roe FILE` if the engagement ROE isn't at the default path.
112
+
113
+ ### Step 2 — Run the scope definition
114
+
115
+ ```bash
116
+ python3 ./scripts/define_scope.py --roe engagements/acme-2026-q2/roe.yaml
117
+ ```
118
+
119
+ Options:
120
+
121
+ ```
122
+ Usage: define_scope.py [OPTIONS]
123
+
124
+ Options:
125
+ --roe FILE Path to ROE YAML (default: ./roe.yaml)
126
+ --emit-allowlist FILE Write flat IP allowlist to FILE
127
+ --emit-targets FILE Write normalized target list to FILE
128
+ --extension FILE Additional scope extension YAML
129
+ --output FILE Findings output
130
+ --format FMT json | jsonl | markdown (default: markdown)
131
+ --min-severity SEV default info
132
+ ```
133
+
134
+ ### Step 3 — Review the conflict report
135
+
136
+ CRITICAL findings (overlap between in-scope and out-of-scope) must
137
+ be resolved before any scan runs. Either narrow the in-scope range
138
+ or remove the out-of-scope overlap; the customer's authorizer
139
+ decides which.
140
+
141
+ HIGH findings (malformed targets, third-party SaaS, reserved
142
+ ranges) require explicit acknowledgement — either fix the entry
143
+ or document in the ROE why the range is intentionally included.
144
+
145
+ ### Step 4 — Hand off the allowlist
146
+
147
+ ```bash
148
+ python3 ./scripts/define_scope.py --roe engagements/acme-2026-q2/roe.yaml \
149
+ --emit-allowlist /tmp/allowed-ips.txt \
150
+ --emit-targets /tmp/normalized-targets.json
151
+ ```
152
+
153
+ The allowlist file is one IP/CIDR per line, ready to paste into:
154
+
155
+ - nmap: `nmap -iL /tmp/allowed-ips.txt`
156
+ - AWS WAF rule: convert to JSON via your standard tooling
157
+ - Burp Suite: paste into Target → Scope → Include
158
+ - This pack's cluster 1 skills: pass via the target argument
159
+
160
+ ## Examples
161
+
162
+ ### Example 1 — Generate scope artifacts for a new engagement
163
+
164
+ ```bash
165
+ python3 ./scripts/define_scope.py \
166
+ --roe engagements/acme-2026-q2/roe.yaml \
167
+ --emit-allowlist engagements/acme-2026-q2/scope/allowed-ips.txt \
168
+ --emit-targets engagements/acme-2026-q2/scope/normalized-targets.json \
169
+ --output engagements/acme-2026-q2/scope/scope-report.md
170
+ ```
171
+
172
+ ### Example 2 — Validate a mid-engagement scope extension
173
+
174
+ ```bash
175
+ python3 ./scripts/define_scope.py \
176
+ --roe engagements/acme-2026-q2/roe.yaml \
177
+ --extension engagements/acme-2026-q2/scope-extension-20260615.yaml
178
+ ```
179
+
180
+ The extension YAML follows the same target format. The skill
181
+ validates that every extension entry has an associated
182
+ authorization reference and emits a CRITICAL finding for any
183
+ extension entry that doesn't.
184
+
185
+ ### Example 3 — Pre-scan validation gate
186
+
187
+ ```bash
188
+ python3 ./scripts/define_scope.py --roe engagements/acme-2026-q2/roe.yaml \
189
+ --min-severity high \
190
+ --format json --output /tmp/scope-issues.json
191
+ jq -e '. == []' /tmp/scope-issues.json || { echo "Scope issues block scan"; exit 1; }
192
+ ```
193
+
194
+ ## Output
195
+
196
+ JSON / JSONL / Markdown per `lib/report.py`. Exit codes: 0 clean,
197
+ 1 high/critical, 2 error.
198
+
199
+ Each Finding includes:
200
+
201
+ - `id` — `scope::<issue>::<target>` (e.g. `scope::malformed::foo[bar`)
202
+ - `severity` — CRITICAL / HIGH / MEDIUM / INFO
203
+ - `category` — `engagement-scope`
204
+ - `summary` — what's wrong with the entry
205
+ - `evidence` — original entry, parsed form, conflict source, line in ROE
206
+
207
+ ## Error Handling
208
+
209
+ - **ROE missing** → emits CRITICAL finding, exits 1.
210
+ - **In-scope section missing or empty** → CRITICAL finding, exits 1.
211
+ - **Unparseable entry** → HIGH finding per entry, scan continues
212
+ for other entries.
213
+ - **Extension file referenced but missing** → HIGH finding.
214
+ - **IPv6 CIDR with very large mask** (e.g. `::/0`) → CRITICAL —
215
+ almost certainly a typo; refuse to expand.
216
+
217
+ ## Resources
218
+
219
+ - `references/THEORY.md` — Why scope is the load-bearing artifact
220
+ of pentest legality, target-type taxonomy, known SaaS-range
221
+ classification (AWS, Cloudflare, GCP, Azure), DNS resolution
222
+ policy (when/whether to resolve at scope-definition time),
223
+ CIDR-overlap detection theory
224
+ - `references/PLAYBOOK.md` — Per-engagement-type scope templates
225
+ (web app, internal network, red team, cloud account, SaaS tenant),
226
+ scope-extension protocol, allowlist-emission patterns per
227
+ scanner, common scope-mistake patterns