drydock-cli 3.0.68__tar.gz → 3.0.69__tar.gz

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 (94) hide show
  1. {drydock_cli-3.0.68/drydock_cli.egg-info → drydock_cli-3.0.69}/PKG-INFO +1 -1
  2. drydock_cli-3.0.69/drydock/builtin_skills/stig-remediate.md +16 -0
  3. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/drydock/rmf_graph.py +32 -0
  4. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/drydock/stig.py +11 -2
  5. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/drydock/tools/__init__.py +16 -4
  6. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/drydock/tui/app.py +20 -1
  7. {drydock_cli-3.0.68 → drydock_cli-3.0.69/drydock_cli.egg-info}/PKG-INFO +1 -1
  8. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/drydock_cli.egg-info/SOURCES.txt +2 -0
  9. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/pyproject.toml +1 -1
  10. drydock_cli-3.0.69/tests/test_rmf_stig_graph.py +39 -0
  11. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/tests/test_skills.py +1 -1
  12. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/LICENSE +0 -0
  13. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/NOTICE +0 -0
  14. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/README.md +0 -0
  15. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/drydock/__init__.py +0 -0
  16. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/drydock/__main__.py +0 -0
  17. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/drydock/agent.py +0 -0
  18. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/drydock/bash_safety.py +0 -0
  19. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/drydock/builtin_skills/__init__.py +0 -0
  20. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/drydock/builtin_skills/rmf-categorize.md +0 -0
  21. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/drydock/builtin_skills/rmf-control.md +0 -0
  22. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/drydock/builtin_skills/rmf-poam.md +0 -0
  23. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/drydock/builtin_skills/rmf-review.md +0 -0
  24. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/drydock/builtin_skills/stig-assess.md +0 -0
  25. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/drydock/cli.py +0 -0
  26. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/drydock/compaction.py +0 -0
  27. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/drydock/config.py +0 -0
  28. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/drydock/detect.py +0 -0
  29. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/drydock/extract.py +0 -0
  30. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/drydock/gittools.py +0 -0
  31. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/drydock/graphrag.py +0 -0
  32. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/drydock/guards.py +0 -0
  33. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/drydock/loop_detect.py +0 -0
  34. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/drydock/mcp.py +0 -0
  35. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/drydock/providers.py +0 -0
  36. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/drydock/rmf.py +0 -0
  37. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/drydock/skills.py +0 -0
  38. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/drydock/tool_registry.py +0 -0
  39. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/drydock/tui/__init__.py +0 -0
  40. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/drydock/tui/approval.py +0 -0
  41. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/drydock/tui/messages.py +0 -0
  42. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/drydock/tui/widgets.py +0 -0
  43. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/drydock/tuning.py +0 -0
  44. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/drydock/web.py +0 -0
  45. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/drydock_cli.egg-info/dependency_links.txt +0 -0
  46. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/drydock_cli.egg-info/entry_points.txt +0 -0
  47. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/drydock_cli.egg-info/requires.txt +0 -0
  48. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/drydock_cli.egg-info/top_level.txt +0 -0
  49. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/setup.cfg +0 -0
  50. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/tests/test_approval.py +0 -0
  51. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/tests/test_back_command.py +0 -0
  52. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/tests/test_bash_output_bounding.py +0 -0
  53. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/tests/test_bash_process_group.py +0 -0
  54. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/tests/test_bash_safety.py +0 -0
  55. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/tests/test_bash_timeout_network.py +0 -0
  56. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/tests/test_cli_agents.py +0 -0
  57. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/tests/test_compact_command.py +0 -0
  58. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/tests/test_compaction.py +0 -0
  59. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/tests/test_config.py +0 -0
  60. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/tests/test_config_migration.py +0 -0
  61. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/tests/test_context_limit_config.py +0 -0
  62. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/tests/test_detect.py +0 -0
  63. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/tests/test_dispatch.py +0 -0
  64. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/tests/test_e2e_connected.py +0 -0
  65. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/tests/test_empty_response.py +0 -0
  66. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/tests/test_extract.py +0 -0
  67. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/tests/test_failure_loop.py +0 -0
  68. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/tests/test_first_run_setup.py +0 -0
  69. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/tests/test_gittools.py +0 -0
  70. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/tests/test_graphrag.py +0 -0
  71. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/tests/test_guards_and_tools.py +0 -0
  72. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/tests/test_hallucinated_tools.py +0 -0
  73. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/tests/test_leaked_tool_call.py +0 -0
  74. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/tests/test_loop_detect.py +0 -0
  75. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/tests/test_mcp.py +0 -0
  76. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/tests/test_oneshot_unreachable.py +0 -0
  77. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/tests/test_plan_autocontinue.py +0 -0
  78. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/tests/test_providers_unreachable.py +0 -0
  79. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/tests/test_read_index.py +0 -0
  80. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/tests/test_rmf.py +0 -0
  81. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/tests/test_rmf_graph.py +0 -0
  82. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/tests/test_runaway_repetition.py +0 -0
  83. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/tests/test_stig.py +0 -0
  84. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/tests/test_stop.py +0 -0
  85. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/tests/test_streaming_newlines.py +0 -0
  86. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/tests/test_subagent.py +0 -0
  87. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/tests/test_system_prompt_help.py +0 -0
  88. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/tests/test_todo.py +0 -0
  89. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/tests/test_tool_arg_parsing.py +0 -0
  90. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/tests/test_tools_undo.py +0 -0
  91. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/tests/test_tui.py +0 -0
  92. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/tests/test_tuning.py +0 -0
  93. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/tests/test_vision_input.py +0 -0
  94. {drydock_cli-3.0.68 → drydock_cli-3.0.69}/tests/test_web_tools.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: drydock-cli
3
- Version: 3.0.68
3
+ Version: 3.0.69
4
4
  Summary: Drydock — a local, provider-agnostic terminal coding agent for local LLMs
5
5
  Author: Frank Bobe III
6
6
  License-Expression: Apache-2.0
@@ -0,0 +1,16 @@
1
+ ---
2
+ name: stig-remediate
3
+ description: Generate a remediation script for an Open STIG finding
4
+ ---
5
+ Generate a remediation script for an Open STIG finding. Input: $ARGS (a checklist
6
+ path and a rule id, e.g. "sys.ckl V-230222").
7
+
8
+ 1. Call StigRule (the path + rule_id) to read the rule's Fix Text and Check Content.
9
+ 2. Determine the target OS — infer from the STIG/checklist name, or ask if unclear.
10
+ 3. Translate the prose Fix Text into an IDEMPOTENT, executable remediation script in
11
+ the requested format (default: Bash for Linux, PowerShell for Windows; Ansible if
12
+ asked). It must bring the system into compliance with the Check Content. Start it
13
+ with a comment header citing the rule id and what it fixes.
14
+ 4. Write the script to a file (Write tool), e.g. remediate_<rule>.sh, and tell the
15
+ user the path. Do NOT run it — the operator reviews and runs remediation.
16
+ Only GENERATE the script; never execute remediation yourself.
@@ -118,6 +118,38 @@ def component_id(name: str) -> str:
118
118
  return f"component:{name.strip().lower()}"
119
119
 
120
120
 
121
+ def stig_id(name: str) -> str:
122
+ return f"stig:{name.strip().lower()}"
123
+
124
+
125
+ def rule_node(rid: str) -> str:
126
+ return f"stigrule:{rid.strip().lower()}"
127
+
128
+
129
+ def ingest_checklist(g: "RmfGraph", checklist) -> dict:
130
+ """Add a parsed STIG Checklist to the typed graph: STIG + STIG-Rule nodes,
131
+ PART_OF (rule→stig), APPLIES_TO (stig→host), EVALUATES (rule→host). Returns
132
+ {rules, host}. The Control —SATISFIED_BY→ STIG-Rule link is asserted
133
+ separately (via GraphAdd 'satisfies') since it needs the CCI→control map."""
134
+ host = (checklist.asset.get("HOST_NAME") or checklist.asset.get("host_name") or "target")
135
+ hnode = component_id(host)
136
+ g.add_node(hnode, "Component", name=host, ip=checklist.asset.get("HOST_IP"))
137
+ sname = checklist.stig_name or "STIG"
138
+ snode = stig_id(sname)
139
+ g.add_node(snode, "STIG", name=sname, version=checklist.stig_version)
140
+ g.add_edge(snode, "APPLIES_TO", hnode)
141
+ for r in checklist.rules:
142
+ rid = r.rule_id or r.group_id
143
+ if not rid:
144
+ continue
145
+ rn = rule_node(rid)
146
+ g.add_node(rn, "STIGRule", rule_id=r.rule_id, group_id=r.group_id,
147
+ severity=r.severity, status=r.status, cci=r.cci, title=r.title)
148
+ g.add_edge(rn, "PART_OF", snode)
149
+ g.add_edge(rn, "EVALUATES", hnode)
150
+ return {"rules": len(checklist.rules), "host": host}
151
+
152
+
121
153
  def build_from_catalog(catalog: dict, *, families: list[str] | None = None) -> RmfGraph:
122
154
  """Deterministically build Control + Objective nodes (and ASSESSES edges) from
123
155
  an OSCAL 800-53 catalog. The typed backbone the user's components/vulns attach to."""
@@ -43,6 +43,7 @@ class Rule:
43
43
  status: str # canonical
44
44
  finding_details: str = ""
45
45
  comments: str = ""
46
+ cci: str = "" # Control Correlation Identifier (maps up to a NIST control)
46
47
  _raw: Any = field(default=None, repr=False) # ET element or dict, for edit-in-place
47
48
 
48
49
  def summary(self) -> str:
@@ -55,6 +56,8 @@ class Checklist:
55
56
  def __init__(self, fmt: str) -> None:
56
57
  self.fmt = fmt
57
58
  self.asset: dict = {}
59
+ self.stig_name: str = ""
60
+ self.stig_version: str = ""
58
61
  self.rules: list[Rule] = []
59
62
  self._tree: Any = None # ckl: ElementTree
60
63
  self._data: Any = None # cklb: dict
@@ -128,6 +131,9 @@ def parse_ckl(path: str | Path) -> Checklist:
128
131
  asset = root.find("ASSET")
129
132
  if asset is not None:
130
133
  cl.asset = {c.tag: (c.text or "") for c in asset}
134
+ info = {si.findtext("SID_NAME"): si.findtext("SID_DATA") for si in root.iter("SI_DATA")}
135
+ cl.stig_name = info.get("title") or info.get("stigid") or ""
136
+ cl.stig_version = info.get("version") or ""
131
137
  for vuln in root.iter("VULN"):
132
138
  data: dict[str, str] = {}
133
139
  for sd in vuln.findall("STIG_DATA"):
@@ -138,7 +144,7 @@ def parse_ckl(path: str | Path) -> Checklist:
138
144
  check_content=data.get("Check_Content", ""), fix_text=data.get("Fix_Text", ""),
139
145
  status=canonical_status(vuln.findtext("STATUS")),
140
146
  finding_details=vuln.findtext("FINDING_DETAILS") or "",
141
- comments=vuln.findtext("COMMENTS") or "", _raw=vuln,
147
+ comments=vuln.findtext("COMMENTS") or "", cci=data.get("CCI_REF", ""), _raw=vuln,
142
148
  ))
143
149
  return cl
144
150
 
@@ -148,7 +154,10 @@ def parse_cklb(path: str | Path) -> Checklist:
148
154
  cl._data = json.loads(Path(path).read_text("utf-8"))
149
155
  cl.asset = cl._data.get("target_data", {}) or {}
150
156
  for stig in cl._data.get("stigs", []) or []:
157
+ cl.stig_name = cl.stig_name or stig.get("stig_name", stig.get("display_name", ""))
158
+ cl.stig_version = cl.stig_version or str(stig.get("version", ""))
151
159
  for rule in stig.get("rules", []) or []:
160
+ ccis = rule.get("ccis") or ([rule["cci"]] if rule.get("cci") else [])
152
161
  cl.rules.append(Rule(
153
162
  group_id=rule.get("group_id", ""), rule_id=rule.get("rule_id", ""),
154
163
  title=rule.get("rule_title", rule.get("group_title", "")),
@@ -156,7 +165,7 @@ def parse_cklb(path: str | Path) -> Checklist:
156
165
  check_content=rule.get("check_content", ""), fix_text=rule.get("fix_text", ""),
157
166
  status=canonical_status(rule.get("status")),
158
167
  finding_details=rule.get("finding_details", ""),
159
- comments=rule.get("comments", ""), _raw=rule,
168
+ comments=rule.get("comments", ""), cci=(ccis[0] if ccis else ""), _raw=rule,
160
169
  ))
161
170
  return cl
162
171
 
@@ -353,10 +353,11 @@ SCHEMAS = [
353
353
  "input_schema": {
354
354
  "type": "object",
355
355
  "properties": {
356
- "op": {"type": "string", "description": "component | implements | resides_on | vulnerability"},
356
+ "op": {"type": "string", "description": "component | implements | resides_on | vulnerability | satisfies"},
357
357
  "component": {"type": "string"},
358
- "control": {"type": "string", "description": "control id, for op=implements"},
358
+ "control": {"type": "string", "description": "control id, for op=implements/satisfies"},
359
359
  "parent": {"type": "string", "description": "parent component/boundary, for op=resides_on"},
360
+ "rule": {"type": "string", "description": "STIG rule id, for op=satisfies"},
360
361
  "id": {"type": "string", "description": "vulnerability/STIG/CVE id, for op=vulnerability"},
361
362
  "severity": {"type": "string"},
362
363
  "os": {"type": "string"}, "ip": {"type": "string"}, "data_type": {"type": "string"},
@@ -1246,6 +1247,9 @@ def tool_graphquery(params: dict, config: dict) -> str:
1246
1247
  if objs:
1247
1248
  out.append("Assessment objectives:\n" + "\n".join(f" - {o}" for o in objs))
1248
1249
  out.append("Implemented by: " + (", ".join(impl) if impl else "(no components recorded)"))
1250
+ rules = [_gattrs(g, rn).get("rule_id", rn) for rn in g.neighbors(node, "SATISFIED_BY", direction="out")]
1251
+ if rules:
1252
+ out.append("Satisfied by STIG rules: " + ", ".join(rules))
1249
1253
  return "\n".join(out)
1250
1254
  if op == "family":
1251
1255
  ctrls = [_gattrs(g, c).get("control_id", c)
@@ -1301,8 +1305,16 @@ def tool_graphadd(params: dict, config: dict) -> str:
1301
1305
  g.add_node(rmf_graph.component_id(comp), "Component", name=comp)
1302
1306
  g.add_edge(vid, "AFFECTS", rmf_graph.component_id(comp))
1303
1307
  g.save(path); return f"Recorded vulnerability {params['id']}" + (f" affecting {comp}." if comp else ".")
1304
- return ("GraphAdd ops: component (name in `component`) | implements (`component`,`control`) "
1305
- "| resides_on (`component`,`parent`) | vulnerability (`id`, optional `component`,`severity`).")
1308
+ if op == "satisfies" and params.get("control") and params.get("rule"):
1309
+ cn, rn = rmf_graph.control_id(params["control"]), rmf_graph.rule_node(params["rule"])
1310
+ if not g.get(cn): # ref node if the catalog isn't bootstrapped
1311
+ g.add_node(cn, "Control", control_id=params["control"].upper())
1312
+ g.add_node(rn, "STIGRule", rule_id=params["rule"])
1313
+ g.add_edge(cn, "SATISFIED_BY", rn)
1314
+ g.save(path); return f"Recorded: {params['control'].upper()} SATISFIED_BY {params['rule']}."
1315
+ return ("GraphAdd ops: component (`component`) | implements (`component`,`control`) | "
1316
+ "resides_on (`component`,`parent`) | vulnerability (`id`, optional `component`) | "
1317
+ "satisfies (`control`,`rule` — a STIG rule satisfies a NIST control).")
1306
1318
 
1307
1319
 
1308
1320
  def tool_websearch(params: dict, config: dict) -> str:
@@ -529,9 +529,28 @@ class DrydockApp(App):
529
529
  "Assess it: /loop <n> /stig-assess <path>")
530
530
  return
531
531
  import os as _os
532
+ cwd = self.config.get("cwd") or "."
533
+ # /stig graph <path> — ingest the checklist into the RMF typed graph
534
+ if parts[0].lower() == "graph" and len(parts) > 1:
535
+ from drydock import rmf_graph
536
+ gp = parts[1] if _os.path.isabs(parts[1]) else _os.path.join(cwd, parts[1])
537
+ try:
538
+ cl = stig.load(gp)
539
+ g = rmf_graph.RmfGraph.load(rmf_graph.graph_path(cwd))
540
+ r = rmf_graph.ingest_checklist(g, cl)
541
+ g.save(rmf_graph.graph_path(cwd))
542
+ except Exception as e: # noqa: BLE001
543
+ self._mount(ErrorMessage(f"could not graph checklist: {e}"))
544
+ return
545
+ self._info(
546
+ f"✓ Ingested {r['rules']} STIG rules for host '{r['host']}' into the "
547
+ f"RMF graph (STIG/STIG-Rule nodes, PART_OF/APPLIES_TO/EVALUATES). "
548
+ "Link rules to controls with GraphAdd satisfies; trace via GraphQuery."
549
+ )
550
+ return
532
551
  path = parts[0]
533
552
  if not _os.path.isabs(path):
534
- path = _os.path.join(self.config.get("cwd") or ".", path)
553
+ path = _os.path.join(cwd, path)
535
554
  try:
536
555
  cl = stig.load(path)
537
556
  except Exception as e: # noqa: BLE001
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: drydock-cli
3
- Version: 3.0.68
3
+ Version: 3.0.69
4
4
  Summary: Drydock — a local, provider-agnostic terminal coding agent for local LLMs
5
5
  Author: Frank Bobe III
6
6
  License-Expression: Apache-2.0
@@ -30,6 +30,7 @@ drydock/builtin_skills/rmf-control.md
30
30
  drydock/builtin_skills/rmf-poam.md
31
31
  drydock/builtin_skills/rmf-review.md
32
32
  drydock/builtin_skills/stig-assess.md
33
+ drydock/builtin_skills/stig-remediate.md
33
34
  drydock/tools/__init__.py
34
35
  drydock/tui/__init__.py
35
36
  drydock/tui/app.py
@@ -74,6 +75,7 @@ tests/test_providers_unreachable.py
74
75
  tests/test_read_index.py
75
76
  tests/test_rmf.py
76
77
  tests/test_rmf_graph.py
78
+ tests/test_rmf_stig_graph.py
77
79
  tests/test_runaway_repetition.py
78
80
  tests/test_skills.py
79
81
  tests/test_stig.py
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
7
7
  # PyPI distribution name is drydock-cli (the established install name, continued
8
8
  # from the retired v2 fork); the import package + CLI command stay `drydock`.
9
9
  name = "drydock-cli"
10
- version = "3.0.68"
10
+ version = "3.0.69"
11
11
  description = "Drydock — a local, provider-agnostic terminal coding agent for local LLMs"
12
12
  readme = "README.md"
13
13
  requires-python = ">=3.11"
@@ -0,0 +1,39 @@
1
+ """RMF Phase 2 STIG ontology — ingest a checklist into the typed graph and
2
+ trace Control <-SATISFIED_BY-> STIG-Rule."""
3
+ from __future__ import annotations
4
+
5
+ from drydock import stig, rmf_graph
6
+ from drydock.tools import tool_graphadd, tool_graphquery
7
+
8
+ _CKL = ('<?xml version="1.0"?><CHECKLIST><ASSET><HOST_NAME>web01</HOST_NAME></ASSET>'
9
+ '<STIGS><iSTIG><STIG_INFO><SI_DATA><SID_NAME>title</SID_NAME><SID_DATA>RHEL 9 STIG</SID_DATA></SI_DATA></STIG_INFO>'
10
+ '<VULN><STIG_DATA><VULN_ATTRIBUTE>Vuln_Num</VULN_ATTRIBUTE><ATTRIBUTE_DATA>V-1</ATTRIBUTE_DATA></STIG_DATA>'
11
+ '<STIG_DATA><VULN_ATTRIBUTE>Rule_ID</VULN_ATTRIBUTE><ATTRIBUTE_DATA>SV-1r1</ATTRIBUTE_DATA></STIG_DATA>'
12
+ '<STIG_DATA><VULN_ATTRIBUTE>CCI_REF</VULN_ATTRIBUTE><ATTRIBUTE_DATA>CCI-000366</ATTRIBUTE_DATA></STIG_DATA>'
13
+ '<STATUS>Open</STATUS></VULN></iSTIG></STIGS></CHECKLIST>')
14
+
15
+
16
+ def test_parse_captures_stig_info_and_cci(tmp_path):
17
+ p = tmp_path / "s.ckl"; p.write_text(_CKL)
18
+ cl = stig.load(p)
19
+ assert cl.stig_name == "RHEL 9 STIG" and cl.rules[0].cci == "CCI-000366"
20
+
21
+
22
+ def test_ingest_checklist_builds_typed_nodes(tmp_path):
23
+ (tmp_path / "s.ckl").write_text(_CKL)
24
+ cl = stig.load(tmp_path / "s.ckl")
25
+ g = rmf_graph.RmfGraph()
26
+ r = rmf_graph.ingest_checklist(g, cl)
27
+ assert r["rules"] == 1 and r["host"] == "web01"
28
+ rn = rmf_graph.rule_node("SV-1r1")
29
+ assert g.get(rn)["type"] == "STIGRule"
30
+ assert rmf_graph.stig_id("RHEL 9 STIG") in g.neighbors(rn, "PART_OF")
31
+ assert rmf_graph.component_id("web01") in g.neighbors(rn, "EVALUATES")
32
+
33
+
34
+ def test_satisfies_links_control_to_rule(tmp_path):
35
+ rmf_graph.RmfGraph().save(rmf_graph.graph_path(str(tmp_path)))
36
+ cfg = {"cwd": str(tmp_path)}
37
+ assert "SATISFIED_BY" in tool_graphadd({"op": "satisfies", "control": "CM-6", "rule": "SV-1r1"}, cfg)
38
+ out = tool_graphquery({"op": "control", "id": "CM-6"}, cfg)
39
+ assert "Satisfied by STIG rules" in out and "SV-1r1" in out
@@ -55,7 +55,7 @@ def test_empty_body_skipped(tmp_path):
55
55
  def test_no_user_or_project_skills_yields_only_builtins(tmp_path):
56
56
  # With no user/project skills, only the bundled built-ins (RMF) are present.
57
57
  loaded = skills.load_skills(str(tmp_path))
58
- assert set(loaded) == {"rmf-control", "rmf-categorize", "rmf-review", "rmf-poam", "stig-assess"}
58
+ assert set(loaded) == {"rmf-control", "rmf-categorize", "rmf-review", "rmf-poam", "stig-assess", "stig-remediate"}
59
59
 
60
60
 
61
61
  def test_create_skill_user_scope(tmp_path, monkeypatch):
File without changes
File without changes
File without changes
File without changes