inspect-ranges 0.2.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 (311) hide show
  1. inspect_ranges/__init__.py +105 -0
  2. inspect_ranges/__main__.py +6 -0
  3. inspect_ranges/_attackbox/__init__.py +48 -0
  4. inspect_ranges/_attackbox/attach.py +293 -0
  5. inspect_ranges/_attackbox/backend.py +218 -0
  6. inspect_ranges/_attackbox/conformance.py +480 -0
  7. inspect_ranges/_attackbox/record.py +90 -0
  8. inspect_ranges/_brand.py +21 -0
  9. inspect_ranges/_build/__init__.py +121 -0
  10. inspect_ranges/_build/activities.py +420 -0
  11. inspect_ranges/_build/behavioral.py +775 -0
  12. inspect_ranges/_build/capture.py +368 -0
  13. inspect_ranges/_build/errors.py +26 -0
  14. inspect_ranges/_build/guesttool.py +292 -0
  15. inspect_ranges/_build/manifest.py +658 -0
  16. inspect_ranges/_build/orchestrate.py +1943 -0
  17. inspect_ranges/_build/recipes.py +671 -0
  18. inspect_ranges/_build/verify.py +611 -0
  19. inspect_ranges/_challenges/__init__.py +175 -0
  20. inspect_ranges/_challenges/attach.py +202 -0
  21. inspect_ranges/_challenges/corroborate.py +294 -0
  22. inspect_ranges/_challenges/dataset.py +121 -0
  23. inspect_ranges/_challenges/engine.py +362 -0
  24. inspect_ranges/_challenges/evidenceread.py +118 -0
  25. inspect_ranges/_challenges/goaltypes.py +275 -0
  26. inspect_ranges/_challenges/netident.py +30 -0
  27. inspect_ranges/_challenges/observedtypes.py +888 -0
  28. inspect_ranges/_challenges/oracle.py +513 -0
  29. inspect_ranges/_challenges/pcapscan.py +78 -0
  30. inspect_ranges/_challenges/plants.py +120 -0
  31. inspect_ranges/_challenges/poller.py +283 -0
  32. inspect_ranges/_challenges/prober.py +197 -0
  33. inspect_ranges/_challenges/registration.py +30 -0
  34. inspect_ranges/_challenges/schema.py +1058 -0
  35. inspect_ranges/_challenges/scorer.py +466 -0
  36. inspect_ranges/_challenges/setup.py +461 -0
  37. inspect_ranges/_challenges/submission.py +81 -0
  38. inspect_ranges/_challenges/timings.py +58 -0
  39. inspect_ranges/_challenges/verify.py +242 -0
  40. inspect_ranges/_channel/__init__.py +95 -0
  41. inspect_ranges/_channel/bundle.py +275 -0
  42. inspect_ranges/_channel/channel.py +1775 -0
  43. inspect_ranges/_channel/codec.py +536 -0
  44. inspect_ranges/_channel/daemon/linux/README.md +44 -0
  45. inspect_ranges/_channel/daemon/windows/README.md +29 -0
  46. inspect_ranges/_channel/mocks.py +424 -0
  47. inspect_ranges/_channel/protocol.py +499 -0
  48. inspect_ranges/_channel/uds.py +154 -0
  49. inspect_ranges/_channel/vsock.py +166 -0
  50. inspect_ranges/_cli/__init__.py +0 -0
  51. inspect_ranges/_cli/appliercmd.py +91 -0
  52. inspect_ranges/_cli/attackboxcmd.py +77 -0
  53. inspect_ranges/_cli/buildcmd.py +197 -0
  54. inspect_ranges/_cli/daemonbundle.py +64 -0
  55. inspect_ranges/_cli/distcmd.py +469 -0
  56. inspect_ranges/_cli/doctor.py +29 -0
  57. inspect_ranges/_cli/evidencecmd.py +116 -0
  58. inspect_ranges/_cli/imagescmd.py +598 -0
  59. inspect_ranges/_cli/main.py +48 -0
  60. inspect_ranges/_cli/plancmd.py +213 -0
  61. inspect_ranges/_cli/reapercmd.py +54 -0
  62. inspect_ranges/_cli/schema.py +11 -0
  63. inspect_ranges/_cli/specpath.py +19 -0
  64. inspect_ranges/_cli/upcmd.py +181 -0
  65. inspect_ranges/_cli/validate.py +75 -0
  66. inspect_ranges/_compiler/__init__.py +32 -0
  67. inspect_ranges/_compiler/acl.py +168 -0
  68. inspect_ranges/_compiler/allocate.py +279 -0
  69. inspect_ranges/_compiler/dnsmasq.py +112 -0
  70. inspect_ranges/_compiler/draws.py +50 -0
  71. inspect_ranges/_compiler/egress.py +109 -0
  72. inspect_ranges/_compiler/nftables.py +104 -0
  73. inspect_ranges/_compiler/plan.py +760 -0
  74. inspect_ranges/_compiler/probe.py +135 -0
  75. inspect_ranges/_compiler/render.py +959 -0
  76. inspect_ranges/_compiler/routing.py +43 -0
  77. inspect_ranges/_devbox/__init__.py +915 -0
  78. inspect_ranges/_devbox/bootstrap.sh +149 -0
  79. inspect_ranges/_devbox/idle-check.sh +49 -0
  80. inspect_ranges/_diagnostics.py +621 -0
  81. inspect_ranges/_dist/__init__.py +54 -0
  82. inspect_ranges/_dist/catalog.py +510 -0
  83. inspect_ranges/_dist/encode.py +435 -0
  84. inspect_ranges/_dist/errors.py +12 -0
  85. inspect_ranges/_dist/gc.py +139 -0
  86. inspect_ranges/_dist/location.py +165 -0
  87. inspect_ranges/_dist/materialize.py +668 -0
  88. inspect_ranges/_dist/mirror.py +216 -0
  89. inspect_ranges/_dist/oci.py +666 -0
  90. inspect_ranges/_dist/progress.py +94 -0
  91. inspect_ranges/_dist/registry.py +775 -0
  92. inspect_ranges/_dist/resolve.py +556 -0
  93. inspect_ranges/_dist/transfer.py +87 -0
  94. inspect_ranges/_doctor/__init__.py +15 -0
  95. inspect_ranges/_doctor/checks.py +1532 -0
  96. inspect_ranges/_doctor/report.py +99 -0
  97. inspect_ranges/_doctor/result.py +28 -0
  98. inspect_ranges/_doctor/s3probe.py +55 -0
  99. inspect_ranges/_evidence/__init__.py +130 -0
  100. inspect_ranges/_evidence/capture.py +743 -0
  101. inspect_ranges/_evidence/execwitness.py +309 -0
  102. inspect_ranges/_evidence/location.py +122 -0
  103. inspect_ranges/_evidence/manifest.py +193 -0
  104. inspect_ranges/_evidence/reader.py +496 -0
  105. inspect_ranges/_evidence/s3.py +214 -0
  106. inspect_ranges/_evidence/sealer.py +149 -0
  107. inspect_ranges/_evidence/sink.py +624 -0
  108. inspect_ranges/_evidence/supervisor.py +241 -0
  109. inspect_ranges/_evidence/telemetry.py +712 -0
  110. inspect_ranges/_host/__init__.py +66 -0
  111. inspect_ranges/_host/applier.py +810 -0
  112. inspect_ranges/_host/backend.py +204 -0
  113. inspect_ranges/_host/host.py +97 -0
  114. inspect_ranges/_host/leases.py +214 -0
  115. inspect_ranges/_host/local.py +223 -0
  116. inspect_ranges/_host/reaper.py +176 -0
  117. inspect_ranges/_host/remote.py +106 -0
  118. inspect_ranges/_host/seams.py +188 -0
  119. inspect_ranges/_host/stages.py +79 -0
  120. inspect_ranges/_metadata.py +10 -0
  121. inspect_ranges/_provider/__init__.py +26 -0
  122. inspect_ranges/_provider/admission.py +150 -0
  123. inspect_ranges/_provider/errors.py +135 -0
  124. inspect_ranges/_provider/evidence.py +570 -0
  125. inspect_ranges/_provider/naming.py +148 -0
  126. inspect_ranges/_provider/ops.py +415 -0
  127. inspect_ranges/_provider/provider.py +911 -0
  128. inspect_ranges/_provider/retry.py +283 -0
  129. inspect_ranges/_provider/state.py +325 -0
  130. inspect_ranges/_registry.py +36 -0
  131. inspect_ranges/_runtime/__init__.py +35 -0
  132. inspect_ranges/_runtime/down.py +189 -0
  133. inspect_ranges/_runtime/images.py +529 -0
  134. inspect_ranges/_runtime/ownership.py +157 -0
  135. inspect_ranges/_runtime/range_image/entrypoint.sh +61 -0
  136. inspect_ranges/_runtime/rangeimage.py +130 -0
  137. inspect_ranges/_runtime/registry.py +80 -0
  138. inspect_ranges/_runtime/up.py +1094 -0
  139. inspect_ranges/_runtime/winimage.py +531 -0
  140. inspect_ranges/_scanners/__init__.py +1 -0
  141. inspect_ranges/_scanners/engine.py +773 -0
  142. inspect_ranges/_scanners/reachability.py +112 -0
  143. inspect_ranges/_terminal/__init__.py +5 -0
  144. inspect_ranges/_terminal/capture.py +280 -0
  145. inspect_ranges/_terminal/cast.py +108 -0
  146. inspect_ranges/_terminal/session.py +395 -0
  147. inspect_ranges/_terminal/tool.py +341 -0
  148. inspect_ranges/_version.py +24 -0
  149. inspect_ranges/attackbox.py +44 -0
  150. inspect_ranges/examples/__init__.py +26 -0
  151. inspect_ranges/examples/_cyris_basic/README.md +132 -0
  152. inspect_ranges/examples/_cyris_basic/__init__.py +1 -0
  153. inspect_ranges/examples/_cyris_basic/challenges.yaml +85 -0
  154. inspect_ranges/examples/_cyris_basic/checks/cross-segment-recon/forged.sh +4 -0
  155. inspect_ranges/examples/_cyris_basic/checks/cross-segment-recon/insufficient.sh +6 -0
  156. inspect_ranges/examples/_cyris_basic/checks/cross-segment-recon/reference.sh +34 -0
  157. inspect_ranges/examples/_cyris_basic/checks/firewall-pivot/forged.sh +4 -0
  158. inspect_ranges/examples/_cyris_basic/checks/firewall-pivot/insufficient.sh +7 -0
  159. inspect_ranges/examples/_cyris_basic/checks/firewall-pivot/reference.sh +97 -0
  160. inspect_ranges/examples/_cyris_basic/checks/forensics-triage/forged.sh +6 -0
  161. inspect_ranges/examples/_cyris_basic/checks/forensics-triage/insufficient.sh +9 -0
  162. inspect_ranges/examples/_cyris_basic/checks/forensics-triage/reference.sh +16 -0
  163. inspect_ranges/examples/_cyris_basic/checks/local-privesc-flag/forged.sh +4 -0
  164. inspect_ranges/examples/_cyris_basic/checks/local-privesc-flag/insufficient.sh +6 -0
  165. inspect_ranges/examples/_cyris_basic/checks/local-privesc-flag/reference.sh +62 -0
  166. inspect_ranges/examples/_cyris_basic/cyris-basic-topology.svg +16 -0
  167. inspect_ranges/examples/_cyris_basic/deferred.yaml +89 -0
  168. inspect_ranges/examples/_cyris_basic/notes.md +51 -0
  169. inspect_ranges/examples/_cyris_basic/range.yaml +87 -0
  170. inspect_ranges/examples/_cyris_basic/recipes/cyris.desktop/recipe.yaml +4 -0
  171. inspect_ranges/examples/_cyris_basic/recipes/cyris.desktop/run.sh +84 -0
  172. inspect_ranges/examples/_cyris_basic/recipes/cyris.firewall/recipe.yaml +4 -0
  173. inspect_ranges/examples/_cyris_basic/recipes/cyris.firewall/run.sh +36 -0
  174. inspect_ranges/examples/_cyris_basic/recipes/cyris.webserver/recipe.yaml +4 -0
  175. inspect_ranges/examples/_cyris_basic/recipes/cyris.webserver/run.sh +79 -0
  176. inspect_ranges/examples/_cyris_basic/source.md +15 -0
  177. inspect_ranges/examples/_cyris_basic/task.py +28 -0
  178. inspect_ranges/examples/_dmz_pivot/README.md +149 -0
  179. inspect_ranges/examples/_dmz_pivot/__init__.py +1 -0
  180. inspect_ranges/examples/_dmz_pivot/challenges.yaml +47 -0
  181. inspect_ranges/examples/_dmz_pivot/checks/crown-jewels/forged.sh +5 -0
  182. inspect_ranges/examples/_dmz_pivot/checks/crown-jewels/insufficient.sh +13 -0
  183. inspect_ranges/examples/_dmz_pivot/checks/crown-jewels/reference.sh +100 -0
  184. inspect_ranges/examples/_dmz_pivot/checks/customer-data/forged.sh +5 -0
  185. inspect_ranges/examples/_dmz_pivot/checks/customer-data/insufficient.sh +18 -0
  186. inspect_ranges/examples/_dmz_pivot/checks/customer-data/reference.sh +102 -0
  187. inspect_ranges/examples/_dmz_pivot/checks/web-foothold/forged.sh +5 -0
  188. inspect_ranges/examples/_dmz_pivot/checks/web-foothold/insufficient.sh +22 -0
  189. inspect_ranges/examples/_dmz_pivot/checks/web-foothold/reference.sh +128 -0
  190. inspect_ranges/examples/_dmz_pivot/dmz-pivot-topology.svg +16 -0
  191. inspect_ranges/examples/_dmz_pivot/notes.md +48 -0
  192. inspect_ranges/examples/_dmz_pivot/range.yaml +115 -0
  193. inspect_ranges/examples/_dmz_pivot/recipes/dmz.db/recipe.yaml +4 -0
  194. inspect_ranges/examples/_dmz_pivot/recipes/dmz.db/run.sh +40 -0
  195. inspect_ranges/examples/_dmz_pivot/recipes/dmz.web/recipe.yaml +4 -0
  196. inspect_ranges/examples/_dmz_pivot/recipes/dmz.web/run.sh +51 -0
  197. inspect_ranges/examples/_dmz_pivot/source.md +8 -0
  198. inspect_ranges/examples/_dmz_pivot/task.py +28 -0
  199. inspect_ranges/examples/_equifax/README.md +131 -0
  200. inspect_ranges/examples/_equifax/__init__.py +1 -0
  201. inspect_ranges/examples/_equifax/challenges.yaml +55 -0
  202. inspect_ranges/examples/_equifax/checks/exfiltrate-critical-data/forged.sh +7 -0
  203. inspect_ranges/examples/_equifax/checks/exfiltrate-critical-data/insufficient.sh +53 -0
  204. inspect_ranges/examples/_equifax/checks/exfiltrate-critical-data/reference.sh +107 -0
  205. inspect_ranges/examples/_equifax/deferred.yaml +83 -0
  206. inspect_ranges/examples/_equifax/equifax-topology.svg +16 -0
  207. inspect_ranges/examples/_equifax/notes.md +55 -0
  208. inspect_ranges/examples/_equifax/range.yaml +191 -0
  209. inspect_ranges/examples/_equifax/recipes/mh.database/payload/pivot_key.pub +1 -0
  210. inspect_ranges/examples/_equifax/recipes/mh.database/recipe.yaml +7 -0
  211. inspect_ranges/examples/_equifax/recipes/mh.database/run.sh +34 -0
  212. inspect_ranges/examples/_equifax/recipes/mh.webserver/payload/pivot_key +8 -0
  213. inspect_ranges/examples/_equifax/recipes/mh.webserver/payload/pivot_key.pub +1 -0
  214. inspect_ranges/examples/_equifax/recipes/mh.webserver/payload/struts_probe.sh +14 -0
  215. inspect_ranges/examples/_equifax/recipes/mh.webserver/recipe.yaml +7 -0
  216. inspect_ranges/examples/_equifax/recipes/mh.webserver/run.sh +71 -0
  217. inspect_ranges/examples/_equifax/source.md +20 -0
  218. inspect_ranges/examples/_equifax/task.py +28 -0
  219. inspect_ranges/examples/_goad_light/README.md +134 -0
  220. inspect_ranges/examples/_goad_light/__init__.py +1 -0
  221. inspect_ranges/examples/_goad_light/challenges.yaml +55 -0
  222. inspect_ranges/examples/_goad_light/checks/ad-domain-dominance/forged.sh +8 -0
  223. inspect_ranges/examples/_goad_light/checks/ad-domain-dominance/insufficient.sh +34 -0
  224. inspect_ranges/examples/_goad_light/checks/ad-domain-dominance/reference.sh +88 -0
  225. inspect_ranges/examples/_goad_light/deferred.yaml +59 -0
  226. inspect_ranges/examples/_goad_light/goad-light-topology.svg +16 -0
  227. inspect_ranges/examples/_goad_light/notes.md +90 -0
  228. inspect_ranges/examples/_goad_light/range.yaml +182 -0
  229. inspect_ranges/examples/_goad_light/recipes/goad.dc01-forest/promote.ps1 +14 -0
  230. inspect_ranges/examples/_goad_light/recipes/goad.dc01-forest/realize.ps1 +259 -0
  231. inspect_ranges/examples/_goad_light/recipes/goad.dc01-forest/recipe.yaml +4 -0
  232. inspect_ranges/examples/_goad_light/recipes/goad.dc01-forest/run.sh +52 -0
  233. inspect_ranges/examples/_goad_light/recipes/goad.dc02-child/promote.ps1 +23 -0
  234. inspect_ranges/examples/_goad_light/recipes/goad.dc02-child/realize.ps1 +163 -0
  235. inspect_ranges/examples/_goad_light/recipes/goad.dc02-child/recipe.yaml +4 -0
  236. inspect_ranges/examples/_goad_light/recipes/goad.dc02-child/run.sh +60 -0
  237. inspect_ranges/examples/_goad_light/recipes/goad.srv02-member/join.ps1 +14 -0
  238. inspect_ranges/examples/_goad_light/recipes/goad.srv02-member/mssql.ps1 +74 -0
  239. inspect_ranges/examples/_goad_light/recipes/goad.srv02-member/realize.ps1 +113 -0
  240. inspect_ranges/examples/_goad_light/recipes/goad.srv02-member/recipe.yaml +4 -0
  241. inspect_ranges/examples/_goad_light/recipes/goad.srv02-member/run.sh +60 -0
  242. inspect_ranges/examples/_goad_light/source.md +17 -0
  243. inspect_ranges/examples/_goad_light/task.py +28 -0
  244. inspect_ranges/examples/_kypo_demo/README.md +132 -0
  245. inspect_ranges/examples/_kypo_demo/__init__.py +1 -0
  246. inspect_ranges/examples/_kypo_demo/challenges.yaml +51 -0
  247. inspect_ranges/examples/_kypo_demo/checks/telnet-ladder/forged.sh +8 -0
  248. inspect_ranges/examples/_kypo_demo/checks/telnet-ladder/insufficient.sh +11 -0
  249. inspect_ranges/examples/_kypo_demo/checks/telnet-ladder/reference.sh +126 -0
  250. inspect_ranges/examples/_kypo_demo/deferred.yaml +58 -0
  251. inspect_ranges/examples/_kypo_demo/kypo-demo-topology.svg +16 -0
  252. inspect_ranges/examples/_kypo_demo/notes.md +47 -0
  253. inspect_ranges/examples/_kypo_demo/range.yaml +95 -0
  254. inspect_ranges/examples/_kypo_demo/recipes/kypo.client/recipe.yaml +4 -0
  255. inspect_ranges/examples/_kypo_demo/recipes/kypo.client/run.sh +51 -0
  256. inspect_ranges/examples/_kypo_demo/recipes/kypo.server/recipe.yaml +7 -0
  257. inspect_ranges/examples/_kypo_demo/recipes/kypo.server/run.sh +80 -0
  258. inspect_ranges/examples/_kypo_demo/source.md +17 -0
  259. inspect_ranges/examples/_kypo_demo/task.py +28 -0
  260. inspect_ranges/examples/_splunk_detection/README.md +126 -0
  261. inspect_ranges/examples/_splunk_detection/__init__.py +1 -0
  262. inspect_ranges/examples/_splunk_detection/activities/benign-admin.ps1 +26 -0
  263. inspect_ranges/examples/_splunk_detection/challenges.yaml +144 -0
  264. inspect_ranges/examples/_splunk_detection/checks/evade-detection-t1003-001/forged.sh +84 -0
  265. inspect_ranges/examples/_splunk_detection/checks/evade-detection-t1003-001/insufficient.sh +40 -0
  266. inspect_ranges/examples/_splunk_detection/checks/evade-detection-t1003-001/reference.sh +130 -0
  267. inspect_ranges/examples/_splunk_detection/checks/execute-and-confirm-t1003-001/forged.sh +47 -0
  268. inspect_ranges/examples/_splunk_detection/checks/execute-and-confirm-t1003-001/insufficient.sh +31 -0
  269. inspect_ranges/examples/_splunk_detection/checks/execute-and-confirm-t1003-001/reference.sh +41 -0
  270. inspect_ranges/examples/_splunk_detection/collectors.yaml +21 -0
  271. inspect_ranges/examples/_splunk_detection/deferred.yaml +54 -0
  272. inspect_ranges/examples/_splunk_detection/notes.md +110 -0
  273. inspect_ranges/examples/_splunk_detection/range.yaml +83 -0
  274. inspect_ranges/examples/_splunk_detection/recipes/arw.splunk-server/recipe.yaml +4 -0
  275. inspect_ranges/examples/_splunk_detection/recipes/arw.splunk-server/run.sh +125 -0
  276. inspect_ranges/examples/_splunk_detection/recipes/arw.win-endpoint/endpoint.ps1 +310 -0
  277. inspect_ranges/examples/_splunk_detection/recipes/arw.win-endpoint/recipe.yaml +6 -0
  278. inspect_ranges/examples/_splunk_detection/recipes/arw.win-endpoint/run.sh +15 -0
  279. inspect_ranges/examples/_splunk_detection/source.md +14 -0
  280. inspect_ranges/examples/_splunk_detection/splunk-detection-topology.svg +16 -0
  281. inspect_ranges/examples/_splunk_detection/task.py +31 -0
  282. inspect_ranges/examples/_vulhub_zabbix/README.md +118 -0
  283. inspect_ranges/examples/_vulhub_zabbix/__init__.py +1 -0
  284. inspect_ranges/examples/_vulhub_zabbix/challenges.yaml +36 -0
  285. inspect_ranges/examples/_vulhub_zabbix/checks/zabbix-rce/forged.sh +6 -0
  286. inspect_ranges/examples/_vulhub_zabbix/checks/zabbix-rce/insufficient.sh +23 -0
  287. inspect_ranges/examples/_vulhub_zabbix/checks/zabbix-rce/reference.sh +105 -0
  288. inspect_ranges/examples/_vulhub_zabbix/deferred.yaml +29 -0
  289. inspect_ranges/examples/_vulhub_zabbix/notes.md +54 -0
  290. inspect_ranges/examples/_vulhub_zabbix/range.yaml +86 -0
  291. inspect_ranges/examples/_vulhub_zabbix/recipes/vulhub.mysql57/recipe.yaml +7 -0
  292. inspect_ranges/examples/_vulhub_zabbix/recipes/vulhub.mysql57/run.sh +73 -0
  293. inspect_ranges/examples/_vulhub_zabbix/recipes/vulhub.zabbix-agent/recipe.yaml +7 -0
  294. inspect_ranges/examples/_vulhub_zabbix/recipes/vulhub.zabbix-agent/run.sh +51 -0
  295. inspect_ranges/examples/_vulhub_zabbix/recipes/vulhub.zabbix-server/recipe.yaml +7 -0
  296. inspect_ranges/examples/_vulhub_zabbix/recipes/vulhub.zabbix-server/run.sh +64 -0
  297. inspect_ranges/examples/_vulhub_zabbix/recipes/vulhub.zabbix-web/recipe.yaml +9 -0
  298. inspect_ranges/examples/_vulhub_zabbix/recipes/vulhub.zabbix-web/run.sh +60 -0
  299. inspect_ranges/examples/_vulhub_zabbix/source.md +15 -0
  300. inspect_ranges/examples/_vulhub_zabbix/task.py +28 -0
  301. inspect_ranges/examples/_vulhub_zabbix/vulhub-zabbix-topology.svg +16 -0
  302. inspect_ranges/host.py +82 -0
  303. inspect_ranges/py.typed +0 -0
  304. inspect_ranges/scanners.py +85 -0
  305. inspect_ranges/schema.py +252 -0
  306. inspect_ranges/types.py +1790 -0
  307. inspect_ranges-0.2.0.dist-info/METADATA +29 -0
  308. inspect_ranges-0.2.0.dist-info/RECORD +311 -0
  309. inspect_ranges-0.2.0.dist-info/WHEEL +4 -0
  310. inspect_ranges-0.2.0.dist-info/entry_points.txt +5 -0
  311. inspect_ranges-0.2.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,105 @@
1
+ # __version__ resolves first: deep imports (the oracle's compiler/runtime
2
+ # chain) read it during package initialization
3
+ try:
4
+ from ._version import __version__
5
+ except ImportError:
6
+ __version__ = "unknown"
7
+
8
+ from ._challenges import (
9
+ PROBE_MAC_PREFIX,
10
+ AnswerGoal,
11
+ AvailabilityGoal,
12
+ Challenge,
13
+ Challenges,
14
+ ControlGoal,
15
+ DegradeGoal,
16
+ DetectionGoal,
17
+ EvadeGoal,
18
+ ExecuteGoal,
19
+ ExfiltrateGoal,
20
+ FlagGoal,
21
+ Goal,
22
+ GoalBase,
23
+ GoalResult,
24
+ GoalType,
25
+ HijackGoal,
26
+ JudgeGoal,
27
+ Observations,
28
+ ObserverKind,
29
+ OracleSetupError,
30
+ PatchGoal,
31
+ Primitive,
32
+ RangeOracle,
33
+ Scoring,
34
+ SeverityFilter,
35
+ TamperGoal,
36
+ challenge_dataset,
37
+ challenge_scorer,
38
+ challenge_setup,
39
+ guest_segment_ip,
40
+ load_challenges,
41
+ load_collectors,
42
+ range_oracle,
43
+ register_exploit,
44
+ register_goal_type,
45
+ source_bound_find,
46
+ validate_challenges,
47
+ verify_goal,
48
+ )
49
+ from ._terminal import terminal
50
+ from .schema import (
51
+ load_range,
52
+ range_json_schema,
53
+ revalidate_range,
54
+ validate_range,
55
+ )
56
+ from .types import semantic_issues
57
+
58
+ __all__ = [
59
+ "PROBE_MAC_PREFIX",
60
+ "AnswerGoal",
61
+ "AvailabilityGoal",
62
+ "Challenge",
63
+ "Challenges",
64
+ "ControlGoal",
65
+ "DegradeGoal",
66
+ "DetectionGoal",
67
+ "EvadeGoal",
68
+ "ExecuteGoal",
69
+ "ExfiltrateGoal",
70
+ "FlagGoal",
71
+ "Goal",
72
+ "GoalBase",
73
+ "GoalResult",
74
+ "GoalType",
75
+ "HijackGoal",
76
+ "JudgeGoal",
77
+ "Observations",
78
+ "ObserverKind",
79
+ "OracleSetupError",
80
+ "PatchGoal",
81
+ "Primitive",
82
+ "RangeOracle",
83
+ "Scoring",
84
+ "SeverityFilter",
85
+ "TamperGoal",
86
+ "__version__",
87
+ "challenge_dataset",
88
+ "challenge_scorer",
89
+ "challenge_setup",
90
+ "guest_segment_ip",
91
+ "load_challenges",
92
+ "load_collectors",
93
+ "load_range",
94
+ "range_json_schema",
95
+ "range_oracle",
96
+ "register_exploit",
97
+ "register_goal_type",
98
+ "revalidate_range",
99
+ "semantic_issues",
100
+ "source_bound_find",
101
+ "terminal",
102
+ "validate_challenges",
103
+ "validate_range",
104
+ "verify_goal",
105
+ ]
@@ -0,0 +1,6 @@
1
+ """`python -m inspect_ranges` — runs the CLI without needing the console script on `PATH`."""
2
+
3
+ from ._cli.main import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
@@ -0,0 +1,48 @@
1
+ """The attack-box provider seam (`attack-box-v1.md`): an external supplier for one range's dedicated attack box.
2
+
3
+ The range supplies an L2 attachment point on the foothold's segment (taps built inside the range netns, fds handed over) and routes the attacker's exec to the surface the provider returns; the provider supplies the box behind both and owns its lifecycle. Registration mirrors `INSPECT_RANGES_HOST`: the `INSPECT_RANGES_ATTACK_BOX` value is the registration, persisted as the project's attack-box origin so teardown re-resolves from disk. Selection is deployment knowledge, never `range.yaml`. Scope is dedicated attack boxes only: an assumed-breach foothold is a content guest this project builds and captures, excluded by construction.
4
+ """
5
+
6
+ from .backend import (
7
+ ENV_VAR,
8
+ AttachedInterface,
9
+ AttackBoxBackend,
10
+ AttackBoxCapabilities,
11
+ AttackBoxContext,
12
+ AttackBoxFactory,
13
+ AttackBoxHandle,
14
+ AttackBoxProvider,
15
+ AttackBoxReclaimFn,
16
+ FootholdAttachment,
17
+ ParsedAttackBox,
18
+ attack_box_origin,
19
+ parse_attack_box,
20
+ resolve_attack_box,
21
+ )
22
+ from .record import (
23
+ AttackBoxRecord,
24
+ attack_box_record_path,
25
+ read_attack_box_record,
26
+ write_attack_box_record,
27
+ )
28
+
29
+ __all__ = [
30
+ "ENV_VAR",
31
+ "AttachedInterface",
32
+ "AttackBoxBackend",
33
+ "AttackBoxCapabilities",
34
+ "AttackBoxContext",
35
+ "AttackBoxFactory",
36
+ "AttackBoxHandle",
37
+ "AttackBoxProvider",
38
+ "AttackBoxReclaimFn",
39
+ "AttackBoxRecord",
40
+ "FootholdAttachment",
41
+ "ParsedAttackBox",
42
+ "attack_box_origin",
43
+ "attack_box_record_path",
44
+ "parse_attack_box",
45
+ "read_attack_box_record",
46
+ "resolve_attack_box",
47
+ "write_attack_box_record",
48
+ ]
@@ -0,0 +1,293 @@
1
+ """The tap-fd attachment: build the external attack box's NICs inside the range netns and hand the fds out.
2
+
3
+ The invariant this module exists to preserve (`architecture.md`: the host netns carries zero range state while a range runs): the external VMM gets its NIC as an OPEN TAP FD created inside the range container's network namespace, never as a device name it cannot resolve and never as a veth or bridge leg reaching into the host netns. A short verbatim helper (`ATTACH_HELPER_SOURCE`, the capture-supervisor pattern) runs inside the range container as root over the same `docker compose exec` transport as the boot script: it opens `/dev/net/tun`, `TUNSETIFF`s one non-persistent tap per attacker interface (so the fd IS the tap's lifetime), applies the libvirt tap-MAC rule, enslaves each tap to its segment bridge, then connects to a host-side unix listener in the shared vsock directory and ships the fds over `SCM_RIGHTS`. The host receives them in the realizing process and hands them to the provider in-process; the fd plumbing never crosses the public seam.
4
+
5
+ The taps are created with `IFF_VNET_HDR`, matching what QEMU and Cloud Hypervisor request when they create taps themselves, so virtio offload negotiation works for an external VMM consuming the fd; a provider that cannot use the header may still read/write raw frames after `TUNSETVNETHDRSZ`. Teardown needs nothing from this module: a non-persistent tap dies when its last fd closes, and unconditionally when the range container's netns is destroyed; `down` tears the provider first, so the external VMM never outlives its NIC.
6
+ """
7
+
8
+ import array
9
+ import os
10
+ import socket
11
+ import subprocess
12
+ import time
13
+ from collections.abc import Callable
14
+ from dataclasses import dataclass
15
+ from ipaddress import IPv4Address
16
+ from pathlib import Path
17
+ from typing import TYPE_CHECKING, Any
18
+
19
+ from .._channel.vsock import peer_uid
20
+ from .backend import AttachedInterface, FootholdAttachment
21
+
22
+ if TYPE_CHECKING:
23
+ from .._runtime.up import BootGuest
24
+
25
+ ATTACH_TIMEOUT_S = 60.0
26
+ """How long the host waits for the in-container helper to build the taps and connect back."""
27
+
28
+ ATTACH_HELPER_SOURCE = '''\
29
+ """Attack-box attach helper: runs as root inside the range container (its netns IS the range netns).
30
+
31
+ Config arrives as JSON in argv[1] (every value shape-validated host-side by `BootPlan` before interpolation). For each interface: open /dev/net/tun, TUNSETIFF a non-persistent IFF_TAP|IFF_NO_PI|IFF_VNET_HDR device, set the fe:-prefixed tap MAC BEFORE enslaving (the bridge adopts its lowest port MAC; a random tap MAC would leak onto the segment), enslave to the segment bridge, bring it up. Then connect to the handoff socket and send one JSON line plus every tap fd via SCM_RIGHTS; wait for the one-byte ack so the fds are known-received before this process (and its fd copies) exits. Any failure tracebacks to stderr and exits nonzero: the host fails the attach stage with it.
32
+ """
33
+
34
+ import array
35
+ import fcntl
36
+ import json
37
+ import socket
38
+ import struct
39
+ import subprocess
40
+ import sys
41
+
42
+ TUNSETIFF = 0x400454CA
43
+ IFF_TAP = 0x0002
44
+ IFF_NO_PI = 0x1000
45
+ IFF_VNET_HDR = 0x4000
46
+
47
+
48
+ def main() -> None:
49
+ config = json.loads(sys.argv[1])
50
+ fds = []
51
+ taps = []
52
+ for spec in config["interfaces"]:
53
+ fd = open("/dev/net/tun", "rb+", buffering=0)
54
+ name = spec["tap"]
55
+ ifr = struct.pack("16sH", name.encode(), IFF_TAP | IFF_NO_PI | IFF_VNET_HDR)
56
+ fcntl.ioctl(fd, TUNSETIFF, ifr)
57
+ subprocess.run(["ip", "link", "set", name, "address", spec["tap_mac"]], check=True)
58
+ subprocess.run(["ip", "link", "set", name, "master", spec["bridge"]], check=True)
59
+ subprocess.run(["ip", "link", "set", name, "up"], check=True)
60
+ fds.append(fd)
61
+ taps.append(name)
62
+ with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as client:
63
+ client.connect(config["socket"])
64
+ payload = json.dumps({"taps": taps}).encode()
65
+ rights = array.array("i", [fd.fileno() for fd in fds])
66
+ client.sendmsg([payload], [(socket.SOL_SOCKET, socket.SCM_RIGHTS, rights.tobytes())])
67
+ ack = client.recv(1)
68
+ if ack != b"1":
69
+ raise SystemExit("handoff not acknowledged; taps die with this process")
70
+
71
+
72
+ main()
73
+ '''
74
+
75
+
76
+ class AttachError(Exception):
77
+ """The attachment did not complete; the message carries the helper's stderr or the handoff failure."""
78
+
79
+
80
+ def tap_name(cid: int, index: int) -> str:
81
+ """The external attack box's per-NIC tap device name: CID-derived like the Cloud Hypervisor scheme (`ch{cid}n{index}`), distinct prefix so a review of a live netns tells the two apart, within IFNAMSIZ."""
82
+ return f"ab{cid}n{index}"
83
+
84
+
85
+ def tap_mac(mac: str) -> str:
86
+ """The tap-device MAC for a guest MAC: first octet `fe`, the libvirt rule the boot script also applies (`up.py`), so the bridge never adopts a MAC that would leak onto the segment."""
87
+ return f"fe{mac[2:]}"
88
+
89
+
90
+ def helper_config(guest: "BootGuest", container_socket: str) -> dict[str, Any]:
91
+ """The attach helper's argv[1] config for one attacker entry: tap name, tap MAC, and bridge per interface, plus the in-container handoff socket path. Pure; every value is already shape-validated by `BootPlan`."""
92
+ return {
93
+ "socket": container_socket,
94
+ "interfaces": [
95
+ {
96
+ "tap": tap_name(guest.cid, index),
97
+ "tap_mac": tap_mac(nic.mac),
98
+ "bridge": nic.bridge,
99
+ }
100
+ for index, nic in enumerate(guest.interfaces)
101
+ ],
102
+ }
103
+
104
+
105
+ @dataclass(frozen=True)
106
+ class AttachInvocation:
107
+ """Everything the attacher needs to run the helper and receive the fds; built by `up`, injectable for tests."""
108
+
109
+ argv: list[str]
110
+ """The `docker compose exec ... python3 - <config-json>` command."""
111
+ env: dict[str, str]
112
+ source: str
113
+ """The helper program, fed on stdin (`ATTACH_HELPER_SOURCE`)."""
114
+ host_socket: Path
115
+ """Where the host listens for the handoff (the shared vsock dir, so the container sees the same socket)."""
116
+ expected_fds: int
117
+ timeout: float = ATTACH_TIMEOUT_S
118
+
119
+
120
+ Attacher = Callable[[AttachInvocation], list[int]]
121
+ """Runs the helper and returns one open tap fd per attacker interface, in interface order. Injectable so `up`'s attach stage is tested without a container; the default is `run_attacher`."""
122
+
123
+
124
+ def receive_fds(listener: socket.socket, expected: int, timeout: float) -> list[int]:
125
+ """Accept the helper's connection and receive `expected` fds via `SCM_RIGHTS`, acking so the sender may exit.
126
+
127
+ The handoff socket sits in the shared vsock directory, which the range container mounts, so every in-container uid (the unprivileged VMMs foremost) can connect to it during the window: the accept loop therefore requires the peer to be the compose-exec root helper (`SO_PEERCRED`, the same check discipline as every exec dial: host uid 0 under rootful docker, the invoking user's uid under rootless, and never a VMM's per-guest uid) and keeps accepting until a credentialed peer or the deadline, so a hostile connector can neither hand over fds nor starve the real helper of its slot. Every blocking step is deadline-bounded (B21: the failure path is as bounded as the boot path), truncated ancillary data (`MSG_CTRUNC`) refuses, and received fds are marked non-inheritable (a provider passing them to its VMM does so explicitly, e.g. `pass_fds`).
128
+
129
+ Raises:
130
+ AttachError: No credentialed helper connected within `timeout`, the handoff was truncated, or it carried the wrong fd count (any received fds are closed before raising; half an attachment is worse than none).
131
+ """
132
+ deadline = time.monotonic() + timeout
133
+ anomaly: str | None = None
134
+
135
+ def remaining() -> float:
136
+ left = deadline - time.monotonic()
137
+ if left <= 0:
138
+ raise AttachError(
139
+ f"helper never completed the handoff within {timeout:.0f}s"
140
+ + (f" (last anomaly: {anomaly})" if anomaly else "")
141
+ )
142
+ return left
143
+
144
+ while True:
145
+ listener.settimeout(remaining())
146
+ try:
147
+ connection, _ = listener.accept()
148
+ except TimeoutError as error:
149
+ raise AttachError(
150
+ f"helper never connected to the handoff socket within {timeout:.0f}s"
151
+ + (f" (last anomaly: {anomaly})" if anomaly else "")
152
+ ) from error
153
+ with connection:
154
+ if peer_uid(connection) not in (0, os.getuid()):
155
+ continue # not the root helper; keep the slot open for it
156
+ # a credentialed stray (another same-uid process probing sockets)
157
+ # must not abort the attach either: bound its recv so it cannot
158
+ # eat the window, note the anomaly, and keep awaiting the helper
159
+ connection.settimeout(min(remaining(), 10.0))
160
+ try:
161
+ space = socket.CMSG_SPACE(expected * array.array("i").itemsize)
162
+ payload, ancillary, flags, _ = connection.recvmsg(4096, space)
163
+ except TimeoutError:
164
+ anomaly = "a credentialed peer connected but sent nothing"
165
+ continue
166
+ fds: list[int] = []
167
+ for level, kind, data in ancillary:
168
+ if level == socket.SOL_SOCKET and kind == socket.SCM_RIGHTS:
169
+ received = array.array("i")
170
+ received.frombytes(
171
+ data[: len(data) - (len(data) % received.itemsize)]
172
+ )
173
+ fds.extend(received.tolist())
174
+ if flags & socket.MSG_CTRUNC or len(fds) != expected:
175
+ for fd in fds:
176
+ os.close(fd)
177
+ anomaly = (
178
+ f"a credentialed peer sent {len(fds)} fds for {expected} "
179
+ f"interfaces"
180
+ f"{' (ancillary truncated)' if flags & socket.MSG_CTRUNC else ''}:"
181
+ f" {payload!r}"
182
+ )
183
+ continue
184
+ for fd in fds:
185
+ os.set_inheritable(fd, False)
186
+ connection.sendall(b"1")
187
+ return fds
188
+
189
+
190
+ def run_attacher(invocation: AttachInvocation) -> list[int]:
191
+ """The default `Attacher`: bind the handoff listener, run the helper in the range container, receive the fds, and require the helper's clean exit.
192
+
193
+ The listener binds BEFORE the helper starts (the helper connects, so there is no race), world-connectable because the container's root is not this uid; the private 0700 per-user root of the vsock dir bounds who can reach the path at all, and the socket is removed as soon as the handoff completes.
194
+
195
+ Raises:
196
+ AttachError: The helper failed, timed out, or the handoff was short; the message carries the helper's stderr tail.
197
+ """
198
+ invocation.host_socket.unlink(missing_ok=True)
199
+ with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as listener:
200
+ listener.bind(str(invocation.host_socket))
201
+ os.chmod(invocation.host_socket, 0o666)
202
+ listener.listen(4)
203
+ process = subprocess.Popen(
204
+ invocation.argv,
205
+ stdin=subprocess.PIPE,
206
+ stdout=subprocess.DEVNULL,
207
+ stderr=subprocess.PIPE,
208
+ env=invocation.env,
209
+ )
210
+ delivered = False
211
+ try:
212
+ assert process.stdin is not None # Popen with stdin=PIPE guarantees it
213
+ # the helper reads its program to EOF before executing, so stdin
214
+ # closes NOW; detach it from the Popen so communicate() below does
215
+ # not flush the already-closed pipe
216
+ process.stdin.write(invocation.source.encode())
217
+ process.stdin.close()
218
+ process.stdin = None
219
+ try:
220
+ fds = receive_fds(listener, invocation.expected_fds, invocation.timeout)
221
+ except AttachError as error:
222
+ process.kill()
223
+ _, stderr = process.communicate(timeout=10.0)
224
+ raise AttachError(
225
+ f"{error} (helper stderr: {stderr.decode('utf-8', 'replace').strip()[-800:]})"
226
+ ) from error
227
+ try:
228
+ _, stderr = process.communicate(timeout=invocation.timeout)
229
+ except subprocess.TimeoutExpired:
230
+ process.kill()
231
+ _, stderr = process.communicate(timeout=10.0)
232
+ if process.returncode != 0:
233
+ for fd in fds:
234
+ os.close(fd)
235
+ raise AttachError(
236
+ "helper exited "
237
+ f"{process.returncode}: {stderr.decode('utf-8', 'replace').strip()[-800:]}"
238
+ )
239
+ delivered = True
240
+ finally:
241
+ invocation.host_socket.unlink(missing_ok=True)
242
+ if not delivered and process.poll() is None:
243
+ # an unexpected escape (connection reset, interrupt) must not
244
+ # orphan the root helper un-reaped
245
+ process.kill()
246
+ try:
247
+ process.communicate(timeout=10.0)
248
+ except subprocess.TimeoutExpired:
249
+ pass
250
+ return fds
251
+
252
+
253
+ def attachment_from_boot(
254
+ guest: "BootGuest",
255
+ *,
256
+ project: str,
257
+ fds: list[int],
258
+ seed_dir: Path | None,
259
+ ) -> FootholdAttachment:
260
+ """Fold one attacker `boot.json` entry plus its received tap fds into the provider's `FootholdAttachment`. Pure.
261
+
262
+ Raises:
263
+ ValueError: The entry lacks the attacker address facts (a bundle rendered before they existed) or sizing; the caller folds this into its re-render refusal.
264
+ """
265
+ if guest.cpus is None or guest.memory_mb is None:
266
+ raise ValueError(f"attacker entry {guest.name!r} carries no sizing")
267
+ interfaces: list[AttachedInterface] = []
268
+ for nic, fd in zip(guest.interfaces, fds, strict=True):
269
+ if nic.network is None or nic.ip is None or nic.prefixlen is None:
270
+ raise ValueError(
271
+ f"attacker entry {guest.name!r} carries no address facts "
272
+ "(bundle rendered before attack-box support); re-render the bundle"
273
+ )
274
+ interfaces.append(
275
+ AttachedInterface(
276
+ segment=nic.network,
277
+ bridge=nic.bridge,
278
+ tap_fd=fd,
279
+ mac=nic.mac,
280
+ ip=IPv4Address(nic.ip),
281
+ prefixlen=nic.prefixlen,
282
+ gateway=IPv4Address(nic.gateway) if nic.gateway is not None else None,
283
+ )
284
+ )
285
+ return FootholdAttachment(
286
+ project=project,
287
+ guest_name=guest.name,
288
+ cid=guest.cid,
289
+ cpus=guest.cpus,
290
+ memory_mb=guest.memory_mb,
291
+ interfaces=tuple(interfaces),
292
+ seed_dir=seed_dir,
293
+ )
@@ -0,0 +1,218 @@
1
+ """Backend selection for `INSPECT_RANGES_ATTACK_BOX`: the grammar, the bundle, and the resolver.
2
+
3
+ The variable's value IS the registration, exactly as `INSPECT_RANGES_HOST` works (`_host/backend.py`): a dotted import path naming an `AttackBoxFactory`, as `<module>.<factory>[:<config>]`, imported and called at resolution. There are no built-in forms; the variable's absence is the built-in behavior (the realizer renders and boots the attack box itself). The value splits on the FIRST colon, so a config may itself contain colons.
4
+
5
+ The value is persisted verbatim as the project's attack-box `origin` (`record.py`), and teardown-by-origin re-resolves it through `resolve_attack_box` in whatever process reclaims: `down` foremost, which may run fresh and never saw the driver's environment. Factories and provider constructors must therefore be cheap and must not dial anything: resolution happens in doctor, in `down`, and before `up` touches any docker resource; remote work belongs in the provider's `boot`.
6
+
7
+ The seam itself is deliberately minimal (every method added is API someone freezes against): an attachment goes in, an exec surface and a teardown obligation come out. The provider's exec surface is held to the daemon wire protocol over a unix socket (the Cloud Hypervisor hybrid-vsock shape, `HybridEndpoint`), which is what makes the published conformance battery, not code review, the gate a provider must pass.
8
+ """
9
+
10
+ import asyncio
11
+ import importlib
12
+ import os
13
+ from collections.abc import Callable
14
+ from dataclasses import dataclass
15
+ from ipaddress import IPv4Address
16
+ from pathlib import Path
17
+ from typing import Any, Protocol
18
+
19
+ from .._channel.vsock import HybridEndpoint
20
+ from .record import AttackBoxRecord
21
+
22
+ ENV_VAR = "INSPECT_RANGES_ATTACK_BOX"
23
+
24
+ DoctorProbe = Callable[[], str | None]
25
+ """An optional reachability probe for `inspect-ranges doctor`: `None` when healthy, else the failure detail (the `_host/backend.py` shape, aliased here so the realizer can import this module without the host-seam dependency chain)."""
26
+
27
+ _GRAMMAR = "(expected a backend factory as '<module>.<factory>[:<config>]')"
28
+
29
+
30
+ @dataclass(frozen=True)
31
+ class AttachedInterface:
32
+ """One L2 attachment point on a foothold segment, built inside the range container's netns.
33
+
34
+ The tap is created and enslaved to the segment bridge inside the range netns before the provider sees it; the provider receives only the open fd. The host netns never carries a device for it, and the attachment dies with the fd (the tap is non-persistent) or with the range container's netns, whichever comes first.
35
+ """
36
+
37
+ segment: str
38
+ """The attached network's name in the plan."""
39
+ bridge: str
40
+ """The segment bridge device name inside the range netns (a fact; the tap is already enslaved)."""
41
+ tap_fd: int
42
+ """The open tap fd. The provider owns it from handoff: pass it to the VMM and close it on teardown."""
43
+ mac: str
44
+ """The allocated guest MAC the external box MUST present on this segment (addressing and ACLs treat it as this guest)."""
45
+ ip: IPv4Address
46
+ """The allocated interface address the external box MUST take."""
47
+ prefixlen: int
48
+ """The segment prefix length."""
49
+ gateway: IPv4Address | None
50
+ """The segment gateway address, when the segment has one."""
51
+
52
+
53
+ @dataclass(frozen=True)
54
+ class FootholdAttachment:
55
+ """What the compiled plan hands an external attack-box provider: attachment in, nothing else.
56
+
57
+ Everything here is a plan fact; the provider supplies its own box (image, VMM, hardening) and attaches it behind the taps. `seed_dir` points at the rendered cloud-init seed for the attack box when the bundle carries one; a provider MAY consume it for in-guest network configuration and is never required to.
58
+ """
59
+
60
+ project: str
61
+ guest_name: str
62
+ cid: int
63
+ """The guest's plan-reserved control-channel CID: a pure addressing token for exec routing, never a uid or tap-name promise."""
64
+ cpus: int
65
+ memory_mb: int
66
+ interfaces: tuple[AttachedInterface, ...]
67
+ seed_dir: Path | None = None
68
+
69
+
70
+ @dataclass(frozen=True)
71
+ class AttackBoxHandle:
72
+ """What a provider's `boot` returns: the exec surface plus a reclaim token, nothing else."""
73
+
74
+ exec_endpoint: HybridEndpoint
75
+ """A unix socket serving the Cloud Hypervisor hybrid-vsock shape: the `CONNECT <port>` / `OK <port>` preamble, then the daemon wire protocol, one request per connection; the peer uid is enforced at every dial."""
76
+ reclaim_token: str = ""
77
+ """Opaque provider state persisted in the attack-box record, handed back verbatim at `teardown`."""
78
+
79
+
80
+ class AttackBoxProvider(Protocol):
81
+ """An external supplier of one range's dedicated attack box.
82
+
83
+ `boot` receives the attachment and returns the running box's handle; it owns the box's lifecycle from that moment. `teardown` must be idempotent (a crash between the record write and `boot` reclaims a box that never existed) and must tolerate a token from a `boot` that half-completed.
84
+ """
85
+
86
+ async def boot(self, attachment: FootholdAttachment) -> AttackBoxHandle: ...
87
+
88
+ async def teardown(self, project: str, reclaim_token: str) -> None: ...
89
+
90
+
91
+ @dataclass(frozen=True)
92
+ class AttackBoxCapabilities:
93
+ """What the backend declares it can do; a declared gap is recorded as evidence, never silently absent."""
94
+
95
+ console_stream: bool = False
96
+ """Whether the provider delivers the attack box's console stream. `False` records a declared gap on `console/<guest>`; `True` is not yet supported (no ingest path) and is refused."""
97
+
98
+
99
+ AttackBoxReclaimFn = Callable[[AttackBoxRecord, Path], None]
100
+ """The teardown-by-origin connector: `(record, state_dir)`; raises on failure, after which `down` refuses to touch the range's own resources (a dead netns strands a live foreign VMM)."""
101
+
102
+
103
+ @dataclass(frozen=True)
104
+ class AttackBoxBackend:
105
+ """One resolved backend bundle: the provider plus the hooks the provider protocol alone cannot carry.
106
+
107
+ `reclaim` overrides the teardown-by-origin connector; left `None`, teardown runs `provider.teardown` with the recorded token. `doctor_probe` lets `inspect-ranges doctor` check reachability. `capabilities` is the declared-drop surface the evidence record and the conformance battery hold the provider to.
108
+ """
109
+
110
+ provider: AttackBoxProvider
111
+ reclaim: AttackBoxReclaimFn | None = None
112
+ doctor_probe: DoctorProbe | None = None
113
+ capabilities: AttackBoxCapabilities = AttackBoxCapabilities()
114
+
115
+ def reclaimer(self) -> AttackBoxReclaimFn:
116
+ """The teardown-by-origin connector `down` runs: the explicit `reclaim`, or the provider's own `teardown` with the recorded token."""
117
+ if self.reclaim is not None:
118
+ return self.reclaim
119
+ provider = self.provider
120
+
121
+ def provider_reclaim(record: AttackBoxRecord, state_dir: Path) -> None:
122
+ asyncio.run(provider.teardown(record.project, record.reclaim_token))
123
+
124
+ return provider_reclaim
125
+
126
+
127
+ @dataclass(frozen=True)
128
+ class AttackBoxContext:
129
+ """What an `AttackBoxFactory` is called with."""
130
+
131
+ origin: str
132
+ """The full `INSPECT_RANGES_ATTACK_BOX` value, verbatim: what `up` stamps into the attack-box record, so a fresh process re-resolves teardown from disk alone."""
133
+ config: str | None
134
+ """The text after the value's first colon, `None` when absent: the backend's own opaque config."""
135
+ state_dir: Path
136
+ """The shared project state dir (the attack-box record lives under it)."""
137
+
138
+
139
+ AttackBoxFactory = Callable[[AttackBoxContext], AttackBoxBackend]
140
+ """What the dotted path in `INSPECT_RANGES_ATTACK_BOX` names. Must be cheap: no dialing, no probing; remote work belongs in the provider's `boot`."""
141
+
142
+
143
+ @dataclass(frozen=True)
144
+ class ParsedAttackBox:
145
+ """One `INSPECT_RANGES_ATTACK_BOX` value, split but not yet resolved."""
146
+
147
+ value: str
148
+ """The full value, verbatim: what `up` stamps as the record's origin."""
149
+ head: str
150
+ """The part before the first colon: the factory's dotted import path."""
151
+ config: str | None
152
+ """The part after the first colon (`None` when absent): opaque backend config."""
153
+
154
+
155
+ def parse_attack_box(value: str) -> ParsedAttackBox:
156
+ """Split an `INSPECT_RANGES_ATTACK_BOX` value against the grammar, without importing anything.
157
+
158
+ Raises:
159
+ ValueError: The value is not a dotted factory path; the message carries the full grammar.
160
+ """
161
+ head, sep, tail = value.partition(":")
162
+ config = tail if sep else None
163
+ if "." in head and all(part.isidentifier() for part in head.split(".")):
164
+ return ParsedAttackBox(value, head, config)
165
+ raise ValueError(f"unknown {ENV_VAR} backend {value!r} {_GRAMMAR}")
166
+
167
+
168
+ def resolve_attack_box(value: str, state_dir: Path) -> AttackBoxBackend:
169
+ """THE resolver: an `INSPECT_RANGES_ATTACK_BOX` value (or a persisted record origin) to its backend bundle.
170
+
171
+ Raises:
172
+ ValueError: The value fits no grammar form, the dotted path does not import or does not name a callable, or the factory returned something other than an `AttackBoxBackend`. A factory's own exception propagates as-is.
173
+ """
174
+ parsed = parse_attack_box(value)
175
+ module_name, _, attr = parsed.head.rpartition(".")
176
+ try:
177
+ module = importlib.import_module(module_name)
178
+ except ImportError as error:
179
+ raise ValueError(
180
+ f"{ENV_VAR}={parsed.value!r}: backend module {module_name!r} does "
181
+ f"not import ({error}); install the package providing it, or fix "
182
+ f"the variable {_GRAMMAR}"
183
+ ) from error
184
+ factory: Any = getattr(module, attr, None)
185
+ if factory is None:
186
+ raise ValueError(
187
+ f"{ENV_VAR}={parsed.value!r}: module {module_name!r} has no "
188
+ f"attribute {attr!r} {_GRAMMAR}"
189
+ )
190
+ if not callable(factory):
191
+ raise ValueError(
192
+ f"{ENV_VAR}={parsed.value!r}: {parsed.head!r} is not callable "
193
+ "(expected an AttackBoxFactory)"
194
+ )
195
+ backend = factory(
196
+ AttackBoxContext(origin=parsed.value, config=parsed.config, state_dir=state_dir)
197
+ )
198
+ if not isinstance(backend, AttackBoxBackend):
199
+ raise ValueError(
200
+ f"{ENV_VAR}={parsed.value!r}: {parsed.head!r} returned "
201
+ f"{type(backend).__name__}, not an AttackBoxBackend"
202
+ )
203
+ return backend
204
+
205
+
206
+ def attack_box_origin() -> str | None:
207
+ """The configured attack-box origin, validated against the grammar; `None` when unset or empty.
208
+
209
+ Validation here is parse-only (no import), so a malformed value refuses loudly wherever the origin is first consulted, before any resource exists.
210
+
211
+ Raises:
212
+ ValueError: The variable is set but fits no grammar form.
213
+ """
214
+ value = os.environ.get(ENV_VAR)
215
+ if not value:
216
+ return None
217
+ parse_attack_box(value)
218
+ return value