drydock-cli 3.0.69__tar.gz → 3.0.71__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.
- {drydock_cli-3.0.69/drydock_cli.egg-info → drydock_cli-3.0.71}/PKG-INFO +1 -1
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/drydock/stig.py +104 -3
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/drydock/tui/app.py +30 -5
- {drydock_cli-3.0.69 → drydock_cli-3.0.71/drydock_cli.egg-info}/PKG-INFO +1 -1
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/drydock_cli.egg-info/SOURCES.txt +2 -1
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/pyproject.toml +1 -1
- drydock_cli-3.0.71/tests/test_xccdf.py +56 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/LICENSE +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/NOTICE +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/README.md +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/drydock/__init__.py +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/drydock/__main__.py +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/drydock/agent.py +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/drydock/bash_safety.py +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/drydock/builtin_skills/__init__.py +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/drydock/builtin_skills/rmf-categorize.md +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/drydock/builtin_skills/rmf-control.md +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/drydock/builtin_skills/rmf-poam.md +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/drydock/builtin_skills/rmf-review.md +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/drydock/builtin_skills/stig-assess.md +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/drydock/builtin_skills/stig-remediate.md +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/drydock/cli.py +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/drydock/compaction.py +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/drydock/config.py +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/drydock/detect.py +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/drydock/extract.py +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/drydock/gittools.py +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/drydock/graphrag.py +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/drydock/guards.py +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/drydock/loop_detect.py +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/drydock/mcp.py +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/drydock/providers.py +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/drydock/rmf.py +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/drydock/rmf_graph.py +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/drydock/skills.py +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/drydock/tool_registry.py +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/drydock/tools/__init__.py +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/drydock/tui/__init__.py +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/drydock/tui/approval.py +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/drydock/tui/messages.py +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/drydock/tui/widgets.py +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/drydock/tuning.py +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/drydock/web.py +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/drydock_cli.egg-info/dependency_links.txt +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/drydock_cli.egg-info/entry_points.txt +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/drydock_cli.egg-info/requires.txt +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/drydock_cli.egg-info/top_level.txt +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/setup.cfg +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/tests/test_approval.py +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/tests/test_back_command.py +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/tests/test_bash_output_bounding.py +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/tests/test_bash_process_group.py +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/tests/test_bash_safety.py +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/tests/test_bash_timeout_network.py +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/tests/test_cli_agents.py +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/tests/test_compact_command.py +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/tests/test_compaction.py +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/tests/test_config.py +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/tests/test_config_migration.py +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/tests/test_context_limit_config.py +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/tests/test_detect.py +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/tests/test_dispatch.py +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/tests/test_e2e_connected.py +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/tests/test_empty_response.py +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/tests/test_extract.py +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/tests/test_failure_loop.py +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/tests/test_first_run_setup.py +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/tests/test_gittools.py +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/tests/test_graphrag.py +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/tests/test_guards_and_tools.py +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/tests/test_hallucinated_tools.py +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/tests/test_leaked_tool_call.py +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/tests/test_loop_detect.py +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/tests/test_mcp.py +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/tests/test_oneshot_unreachable.py +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/tests/test_plan_autocontinue.py +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/tests/test_providers_unreachable.py +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/tests/test_read_index.py +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/tests/test_rmf.py +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/tests/test_rmf_graph.py +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/tests/test_rmf_stig_graph.py +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/tests/test_runaway_repetition.py +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/tests/test_skills.py +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/tests/test_stig.py +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/tests/test_stop.py +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/tests/test_streaming_newlines.py +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/tests/test_subagent.py +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/tests/test_system_prompt_help.py +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/tests/test_todo.py +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/tests/test_tool_arg_parsing.py +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/tests/test_tools_undo.py +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/tests/test_tui.py +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/tests/test_tuning.py +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/tests/test_vision_input.py +0 -0
- {drydock_cli-3.0.69 → drydock_cli-3.0.71}/tests/test_web_tools.py +0 -0
|
@@ -12,6 +12,7 @@ Stdlib only (xml.etree + json). All logic original to Drydock.
|
|
|
12
12
|
from __future__ import annotations
|
|
13
13
|
|
|
14
14
|
import json
|
|
15
|
+
import re
|
|
15
16
|
import xml.etree.ElementTree as ET
|
|
16
17
|
from dataclasses import dataclass, field
|
|
17
18
|
from pathlib import Path
|
|
@@ -124,10 +125,10 @@ def _set_child_text(el: ET.Element, tag: str, text: str) -> None:
|
|
|
124
125
|
|
|
125
126
|
|
|
126
127
|
# ── parsing ─────────────────────────────────────────────────────────────────
|
|
127
|
-
def
|
|
128
|
+
def _checklist_from_tree(tree: Any) -> Checklist:
|
|
128
129
|
cl = Checklist("ckl")
|
|
129
|
-
cl._tree =
|
|
130
|
-
root =
|
|
130
|
+
cl._tree = tree
|
|
131
|
+
root = tree.getroot()
|
|
131
132
|
asset = root.find("ASSET")
|
|
132
133
|
if asset is not None:
|
|
133
134
|
cl.asset = {c.tag: (c.text or "") for c in asset}
|
|
@@ -149,6 +150,106 @@ def parse_ckl(path: str | Path) -> Checklist:
|
|
|
149
150
|
return cl
|
|
150
151
|
|
|
151
152
|
|
|
153
|
+
def parse_ckl(path: str | Path) -> Checklist:
|
|
154
|
+
return _checklist_from_tree(ET.parse(path))
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
# ── XCCDF benchmark (the raw STIG) → a blank .ckl ───────────────────────────
|
|
158
|
+
_VULN_DISCUSS = re.compile(r"<VulnDiscussion>(.*?)</VulnDiscussion>", re.S | re.I)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _lname(el) -> str:
|
|
162
|
+
return el.tag.split("}")[-1]
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _kids(parent, name):
|
|
166
|
+
return [c for c in parent if _lname(c) == name]
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _ktext(parent, name) -> str:
|
|
170
|
+
for c in parent:
|
|
171
|
+
if _lname(c) == name:
|
|
172
|
+
return (c.text or "").strip()
|
|
173
|
+
return ""
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def parse_xccdf(path: str | Path) -> dict:
|
|
177
|
+
"""Parse a DISA STIG XCCDF benchmark (namespace-agnostic) into
|
|
178
|
+
{title, version, release, stigid, rules:[...]}."""
|
|
179
|
+
root = ET.parse(path).getroot()
|
|
180
|
+
title = _ktext(root, "title")
|
|
181
|
+
version = _ktext(root, "version")
|
|
182
|
+
stigid = root.get("id", "")
|
|
183
|
+
release = ""
|
|
184
|
+
for pt in _kids(root, "plain-text"):
|
|
185
|
+
if pt.get("id") == "release-info":
|
|
186
|
+
release = (pt.text or "").strip()
|
|
187
|
+
rules = []
|
|
188
|
+
for group in _kids(root, "Group"):
|
|
189
|
+
gid = group.get("id", "")
|
|
190
|
+
gtitle = _ktext(group, "title")
|
|
191
|
+
for rule in _kids(group, "Rule"):
|
|
192
|
+
desc = _ktext(rule, "description")
|
|
193
|
+
m = _VULN_DISCUSS.search(desc)
|
|
194
|
+
check = ""
|
|
195
|
+
for ch in _kids(rule, "check"):
|
|
196
|
+
check = _ktext(ch, "check-content") or check
|
|
197
|
+
rules.append({
|
|
198
|
+
"group_id": gid, "group_title": gtitle,
|
|
199
|
+
"rule_id": rule.get("id", ""), "rule_ver": _ktext(rule, "version"),
|
|
200
|
+
"severity": rule.get("severity", ""), "weight": rule.get("weight", ""),
|
|
201
|
+
"title": _ktext(rule, "title"),
|
|
202
|
+
"discussion": (m.group(1).strip() if m else desc),
|
|
203
|
+
"check": check, "fix": _ktext(rule, "fixtext"),
|
|
204
|
+
"ccis": [(c.text or "").strip() for c in _kids(rule, "ident")
|
|
205
|
+
if "cci" in (c.get("system", "") + (c.text or "")).lower()],
|
|
206
|
+
})
|
|
207
|
+
return {"title": title, "version": version, "release": release,
|
|
208
|
+
"stigid": stigid, "rules": rules}
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def xccdf_to_checklist(path: str | Path, *, host: str = "") -> Checklist:
|
|
212
|
+
"""Build a blank .ckl (all rules Not_Reviewed) from a STIG XCCDF benchmark —
|
|
213
|
+
the inverse of what STIG Viewer does, so the catalog can be assessed directly."""
|
|
214
|
+
bench = parse_xccdf(path)
|
|
215
|
+
stigref = f"{bench['title']} :: Version {bench['version']}, {bench['release']}".strip(" :,")
|
|
216
|
+
root = ET.Element("CHECKLIST")
|
|
217
|
+
asset = ET.SubElement(root, "ASSET")
|
|
218
|
+
for tag, val in [("ROLE", "None"), ("ASSET_TYPE", "Computing"), ("HOST_NAME", host),
|
|
219
|
+
("HOST_IP", ""), ("HOST_MAC", ""), ("HOST_FQDN", ""),
|
|
220
|
+
("TARGET_COMMENT", ""), ("TECH_AREA", ""), ("TARGET_KEY", ""),
|
|
221
|
+
("WEB_OR_DATABASE", "false"), ("WEB_DB_SITE", ""), ("WEB_DB_INSTANCE", "")]:
|
|
222
|
+
ET.SubElement(asset, tag).text = val
|
|
223
|
+
istig = ET.SubElement(ET.SubElement(root, "STIGS"), "iSTIG")
|
|
224
|
+
info = ET.SubElement(istig, "STIG_INFO")
|
|
225
|
+
for name, data in [("version", bench["version"]), ("classification", "UNCLASSIFIED"),
|
|
226
|
+
("stigid", bench["stigid"]), ("description", ""),
|
|
227
|
+
("releaseinfo", bench["release"]), ("title", bench["title"])]:
|
|
228
|
+
sid = ET.SubElement(info, "SI_DATA")
|
|
229
|
+
ET.SubElement(sid, "SID_NAME").text = name
|
|
230
|
+
ET.SubElement(sid, "SID_DATA").text = data
|
|
231
|
+
for rd in bench["rules"]:
|
|
232
|
+
vuln = ET.SubElement(istig, "VULN")
|
|
233
|
+
for attr, val in [("Vuln_Num", rd["group_id"]), ("Severity", rd["severity"]),
|
|
234
|
+
("Group_Title", rd["group_title"]), ("Rule_ID", rd["rule_id"]),
|
|
235
|
+
("Rule_Ver", rd["rule_ver"]), ("Rule_Title", rd["title"]),
|
|
236
|
+
("Vuln_Discuss", rd["discussion"]), ("Check_Content", rd["check"]),
|
|
237
|
+
("Fix_Text", rd["fix"]), ("Weight", rd["weight"]), ("STIGRef", stigref)]:
|
|
238
|
+
sd = ET.SubElement(vuln, "STIG_DATA")
|
|
239
|
+
ET.SubElement(sd, "VULN_ATTRIBUTE").text = attr
|
|
240
|
+
ET.SubElement(sd, "ATTRIBUTE_DATA").text = val or ""
|
|
241
|
+
for cci in rd["ccis"]:
|
|
242
|
+
sd = ET.SubElement(vuln, "STIG_DATA")
|
|
243
|
+
ET.SubElement(sd, "VULN_ATTRIBUTE").text = "CCI_REF"
|
|
244
|
+
ET.SubElement(sd, "ATTRIBUTE_DATA").text = cci
|
|
245
|
+
ET.SubElement(vuln, "STATUS").text = "Not_Reviewed"
|
|
246
|
+
ET.SubElement(vuln, "FINDING_DETAILS").text = ""
|
|
247
|
+
ET.SubElement(vuln, "COMMENTS").text = ""
|
|
248
|
+
ET.SubElement(vuln, "SEVERITY_OVERRIDE").text = ""
|
|
249
|
+
ET.SubElement(vuln, "SEVERITY_JUSTIFICATION").text = ""
|
|
250
|
+
return _checklist_from_tree(ET.ElementTree(root))
|
|
251
|
+
|
|
252
|
+
|
|
152
253
|
def parse_cklb(path: str | Path) -> Checklist:
|
|
153
254
|
cl = Checklist("cklb")
|
|
154
255
|
cl._data = json.loads(Path(path).read_text("utf-8"))
|
|
@@ -496,7 +496,7 @@ class DrydockApp(App):
|
|
|
496
496
|
" /loop /loop <count> <prompt> — repeat a prompt (Esc stops)\n"
|
|
497
497
|
" /mcp list connected MCP servers and their tools\n"
|
|
498
498
|
" /rmf RMF automation — /rmf bootstrap, then /rmf-control etc.\n"
|
|
499
|
-
" /stig
|
|
499
|
+
" /stig /stig new <xccdf> → blank .ckl; summarize; /stig-assess\n"
|
|
500
500
|
" /clear reset the conversation\n"
|
|
501
501
|
" /quit exit\n"
|
|
502
502
|
"Type a task and press Enter. ↑/↓ recall history · Esc stops · Ctrl+O expands tools."
|
|
@@ -524,12 +524,32 @@ class DrydockApp(App):
|
|
|
524
524
|
|
|
525
525
|
parts = arg.split()
|
|
526
526
|
if not parts:
|
|
527
|
-
self._info("usage
|
|
528
|
-
"
|
|
529
|
-
"
|
|
527
|
+
self._info("usage:\n"
|
|
528
|
+
" /stig new <benchmark-xccdf.xml> [out.ckl] — blank .ckl from a STIG\n"
|
|
529
|
+
" /stig <path.ckl|.cklb> [status] — summary / list by status\n"
|
|
530
|
+
" /stig graph <path.ckl> — ingest into the RMF graph\n"
|
|
531
|
+
"Assess: /loop <n> /stig-assess <path>")
|
|
530
532
|
return
|
|
531
533
|
import os as _os
|
|
532
534
|
cwd = self.config.get("cwd") or "."
|
|
535
|
+
_abs = lambda p: p if _os.path.isabs(p) else _os.path.join(cwd, p) # noqa: E731
|
|
536
|
+
# /stig new <xccdf> [out.ckl] — generate a BLANK .ckl from a STIG XCCDF benchmark
|
|
537
|
+
if parts[0].lower() == "new" and len(parts) > 1:
|
|
538
|
+
xp = _abs(parts[1])
|
|
539
|
+
out = _abs(parts[2]) if len(parts) > 2 else \
|
|
540
|
+
_os.path.splitext(_abs(parts[1]))[0].replace("-xccdf", "") + ".ckl"
|
|
541
|
+
try:
|
|
542
|
+
cl = stig.xccdf_to_checklist(xp)
|
|
543
|
+
cl.save(out)
|
|
544
|
+
except Exception as e: # noqa: BLE001
|
|
545
|
+
self._mount(ErrorMessage(f"could not parse the STIG XCCDF: {e}"))
|
|
546
|
+
return
|
|
547
|
+
self._info(
|
|
548
|
+
f"✓ Generated {out} from the STIG benchmark — {len(cl.rules)} rules, "
|
|
549
|
+
"all Not_Reviewed. Now pull in the app's evidence and assess:\n"
|
|
550
|
+
f" /graphrag build <app-docs> · /loop {len(cl.rules)} /stig-assess {out}"
|
|
551
|
+
)
|
|
552
|
+
return
|
|
533
553
|
# /stig graph <path> — ingest the checklist into the RMF typed graph
|
|
534
554
|
if parts[0].lower() == "graph" and len(parts) > 1:
|
|
535
555
|
from drydock import rmf_graph
|
|
@@ -803,7 +823,12 @@ class DrydockApp(App):
|
|
|
803
823
|
self._info("No knowledge base yet. Build one: /graphrag build <path>")
|
|
804
824
|
return
|
|
805
825
|
res = graphrag.query_index(index, rest, k=3)
|
|
806
|
-
self._info(
|
|
826
|
+
self._info(
|
|
827
|
+
graphrag.format_results(res, rest)
|
|
828
|
+
+ "\n\n— This is a PREVIEW for you; it does not go to the model. "
|
|
829
|
+
"The agent retrieves from this knowledge base automatically (via "
|
|
830
|
+
"its Knowledge tool) when you just ASK a question — no slash command."
|
|
831
|
+
)
|
|
807
832
|
elif sub in ("", "status"):
|
|
808
833
|
index = graphrag.load_index(store)
|
|
809
834
|
if index is None:
|
|
@@ -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.
|
|
10
|
+
version = "3.0.71"
|
|
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,56 @@
|
|
|
1
|
+
"""XCCDF benchmark (raw STIG) -> blank .ckl generator — the inverse of STIG
|
|
2
|
+
Viewer, closing the loop: STIG benchmark -> assess -> completed .ckl."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import xml.etree.ElementTree as ET
|
|
6
|
+
|
|
7
|
+
from drydock import stig
|
|
8
|
+
|
|
9
|
+
# A minimal but real-shaped DISA STIG XCCDF (namespaced, like the live files).
|
|
10
|
+
_XCCDF = '''<?xml version="1.0"?>
|
|
11
|
+
<Benchmark xmlns="http://checklists.nist.gov/xccdf/1.1" id="ASD_STIG">
|
|
12
|
+
<title>Application Security and Development STIG</title>
|
|
13
|
+
<version>6</version>
|
|
14
|
+
<plain-text id="release-info">Release: 1 Benchmark Date: 24 Jan 2024</plain-text>
|
|
15
|
+
<Group id="V-222387">
|
|
16
|
+
<title>SRG-APP-000001</title>
|
|
17
|
+
<Rule id="SV-222387r879511_rule" severity="medium" weight="10.0">
|
|
18
|
+
<version>APSC-DV-000010</version>
|
|
19
|
+
<title>The application must limit the number of concurrent sessions.</title>
|
|
20
|
+
<description><VulnDiscussion>Limiting sessions reduces risk.</VulnDiscussion><FalsePositives></FalsePositives></description>
|
|
21
|
+
<ident system="http://cyber.mil/cci">CCI-000054</ident>
|
|
22
|
+
<fixtext fixref="F-1">Configure the application to limit concurrent sessions.</fixtext>
|
|
23
|
+
<check system="C-1"><check-content>Review the application configuration for a session limit.</check-content></check>
|
|
24
|
+
</Rule>
|
|
25
|
+
</Group>
|
|
26
|
+
</Benchmark>'''
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_parse_xccdf(tmp_path):
|
|
30
|
+
p = tmp_path / "b-xccdf.xml"; p.write_text(_XCCDF)
|
|
31
|
+
b = stig.parse_xccdf(p)
|
|
32
|
+
assert b["title"].startswith("Application Security") and b["version"] == "6"
|
|
33
|
+
assert "Release: 1" in b["release"]
|
|
34
|
+
r = b["rules"][0]
|
|
35
|
+
assert r["group_id"] == "V-222387" and r["rule_id"] == "SV-222387r879511_rule"
|
|
36
|
+
assert r["rule_ver"] == "APSC-DV-000010" and r["severity"] == "medium"
|
|
37
|
+
assert r["discussion"] == "Limiting sessions reduces risk." # VulnDiscussion extracted
|
|
38
|
+
assert "session limit" in r["check"] and "concurrent sessions" in r["fix"]
|
|
39
|
+
assert r["ccis"] == ["CCI-000054"]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_xccdf_to_blank_ckl(tmp_path):
|
|
43
|
+
p = tmp_path / "b-xccdf.xml"; p.write_text(_XCCDF)
|
|
44
|
+
cl = stig.xccdf_to_checklist(p, host="appsrv1")
|
|
45
|
+
assert len(cl.rules) == 1 and cl.counts()["not_reviewed"] == 1
|
|
46
|
+
assert cl.rules[0].rule_id == "SV-222387r879511_rule" and cl.rules[0].cci == "CCI-000054"
|
|
47
|
+
out = tmp_path / "gen.ckl"; cl.save(out)
|
|
48
|
+
ET.parse(out) # valid XML
|
|
49
|
+
# round-trips through the engine and is assessable
|
|
50
|
+
re = stig.load(out)
|
|
51
|
+
assert re.rules[0].check_content.startswith("Review the application")
|
|
52
|
+
assert re.asset["HOST_NAME"] == "appsrv1"
|
|
53
|
+
re.update("V-222387", status="open", finding_details="no limit configured"); re.save(out)
|
|
54
|
+
assert stig.load(out).rules[0].status == "open"
|
|
55
|
+
# the regenerated .ckl preserves the DISA status enum
|
|
56
|
+
assert "<STATUS>Open</STATUS>" in out.read_text()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|