drydock-cli 3.0.72__tar.gz → 3.0.73__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.72/drydock_cli.egg-info → drydock_cli-3.0.73}/PKG-INFO +1 -1
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/drydock/stig.py +29 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/drydock/tui/app.py +2 -13
- {drydock_cli-3.0.72 → drydock_cli-3.0.73/drydock_cli.egg-info}/PKG-INFO +1 -1
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/pyproject.toml +1 -1
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/tests/test_mcp.py +30 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/tests/test_rmf_graph.py +26 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/tests/test_stig.py +48 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/tests/test_web_tools.py +33 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/tests/test_xccdf.py +29 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/LICENSE +0 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/NOTICE +0 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/README.md +0 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/drydock/__init__.py +0 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/drydock/__main__.py +0 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/drydock/agent.py +0 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/drydock/bash_safety.py +0 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/drydock/builtin_skills/__init__.py +0 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/drydock/builtin_skills/rmf-categorize.md +0 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/drydock/builtin_skills/rmf-control.md +0 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/drydock/builtin_skills/rmf-poam.md +0 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/drydock/builtin_skills/rmf-review.md +0 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/drydock/builtin_skills/stig-assess.md +0 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/drydock/builtin_skills/stig-remediate.md +0 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/drydock/cli.py +0 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/drydock/compaction.py +0 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/drydock/config.py +0 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/drydock/detect.py +0 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/drydock/extract.py +0 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/drydock/gittools.py +0 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/drydock/graphrag.py +0 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/drydock/guards.py +0 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/drydock/loop_detect.py +0 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/drydock/mcp.py +0 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/drydock/providers.py +0 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/drydock/rmf.py +0 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/drydock/rmf_graph.py +0 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/drydock/skills.py +0 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/drydock/tool_registry.py +0 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/drydock/tools/__init__.py +0 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/drydock/tui/__init__.py +0 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/drydock/tui/approval.py +0 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/drydock/tui/messages.py +0 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/drydock/tui/widgets.py +0 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/drydock/tuning.py +0 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/drydock/web.py +0 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/drydock_cli.egg-info/SOURCES.txt +0 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/drydock_cli.egg-info/dependency_links.txt +0 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/drydock_cli.egg-info/entry_points.txt +0 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/drydock_cli.egg-info/requires.txt +0 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/drydock_cli.egg-info/top_level.txt +0 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/setup.cfg +0 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/tests/test_approval.py +0 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/tests/test_back_command.py +0 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/tests/test_bash_output_bounding.py +0 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/tests/test_bash_process_group.py +0 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/tests/test_bash_safety.py +0 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/tests/test_bash_timeout_network.py +0 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/tests/test_cli_agents.py +0 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/tests/test_compact_command.py +0 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/tests/test_compaction.py +0 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/tests/test_config.py +0 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/tests/test_config_migration.py +0 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/tests/test_context_limit_config.py +0 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/tests/test_detect.py +0 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/tests/test_dispatch.py +0 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/tests/test_e2e_connected.py +0 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/tests/test_empty_response.py +0 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/tests/test_extract.py +0 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/tests/test_failure_loop.py +0 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/tests/test_first_run_setup.py +0 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/tests/test_gittools.py +0 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/tests/test_graphrag.py +0 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/tests/test_guards_and_tools.py +0 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/tests/test_hallucinated_tools.py +0 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/tests/test_leaked_tool_call.py +0 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/tests/test_loop_detect.py +0 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/tests/test_oneshot_unreachable.py +0 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/tests/test_plan_autocontinue.py +0 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/tests/test_providers_unreachable.py +0 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/tests/test_read_index.py +0 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/tests/test_rmf.py +0 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/tests/test_rmf_stig_graph.py +0 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/tests/test_runaway_repetition.py +0 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/tests/test_skills.py +0 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/tests/test_stop.py +0 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/tests/test_streaming_newlines.py +0 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/tests/test_subagent.py +0 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/tests/test_system_prompt_help.py +0 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/tests/test_todo.py +0 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/tests/test_tool_arg_parsing.py +0 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/tests/test_tools_undo.py +0 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/tests/test_tui.py +0 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/tests/test_tuning.py +0 -0
- {drydock_cli-3.0.72 → drydock_cli-3.0.73}/tests/test_vision_input.py +0 -0
|
@@ -281,3 +281,32 @@ def load(path: str | Path) -> Checklist:
|
|
|
281
281
|
# sniff: JSON → cklb, else ckl
|
|
282
282
|
head = Path(path).read_text("utf-8", "ignore").lstrip()[:1]
|
|
283
283
|
return parse_cklb(path) if head == "{" else parse_ckl(path)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def summary_lines(cl: "Checklist", label: str, status: str | None = None) -> list[str]:
|
|
287
|
+
"""Build the `/stig` summary view: counts, an optional status listing (never
|
|
288
|
+
silently truncated), and scale-aware next-step hints. Pure/presentation so it
|
|
289
|
+
can be unit-tested without the TUI."""
|
|
290
|
+
host = cl.asset.get("HOST_NAME") or cl.asset.get("host_name") or "?"
|
|
291
|
+
c = cl.counts()
|
|
292
|
+
lines = [f"STIG checklist {label} (host: {host}, format: {cl.fmt})",
|
|
293
|
+
f" {len(cl.rules)} rules — " + " · ".join(f"{k}={v}" for k, v in c.items())]
|
|
294
|
+
if status:
|
|
295
|
+
sf = canonical_status(status)
|
|
296
|
+
hits = [r for r in cl.rules if r.status == sf]
|
|
297
|
+
lines.append(f"\n{sf} ({len(hits)}):")
|
|
298
|
+
lines += [f" {r.group_id} ({r.severity}) — {r.title}" for r in hits[:50]]
|
|
299
|
+
if len(hits) > 50: # never silently truncate
|
|
300
|
+
lines.append(f" … showing first 50 of {len(hits)}; "
|
|
301
|
+
"open the .ckl in STIG Viewer for the full list.")
|
|
302
|
+
return lines
|
|
303
|
+
nr = c.get("not_reviewed", 0)
|
|
304
|
+
if nr:
|
|
305
|
+
lines.append(f"\nAssess the {nr} un-reviewed rule(s) — one focused turn each:"
|
|
306
|
+
f"\n /loop {nr} /stig-assess {label}")
|
|
307
|
+
if nr > 40:
|
|
308
|
+
lines.append(f" (large checklist — that's {nr} model turns; you can run it in "
|
|
309
|
+
"batches and re-run /stig to see remaining counts.)")
|
|
310
|
+
if c.get("open"):
|
|
311
|
+
lines.append(f"List open findings to remediate: /stig {label} open")
|
|
312
|
+
return lines
|
|
@@ -587,19 +587,8 @@ class DrydockApp(App):
|
|
|
587
587
|
except Exception as e: # noqa: BLE001
|
|
588
588
|
self._mount(ErrorMessage(f"could not read checklist: {e}"))
|
|
589
589
|
return
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
lines = [f"STIG checklist {path} (host: {host}, format: {cl.fmt})",
|
|
593
|
-
f" {len(cl.rules)} rules — " + " · ".join(f"{k}={v}" for k, v in c.items())]
|
|
594
|
-
if len(parts) > 1:
|
|
595
|
-
sf = stig.canonical_status(parts[1])
|
|
596
|
-
hits = [r for r in cl.rules if r.status == sf]
|
|
597
|
-
lines.append(f"\n{sf} ({len(hits)}):")
|
|
598
|
-
lines += [f" {r.group_id} ({r.severity}) — {r.title}" for r in hits[:50]]
|
|
599
|
-
else:
|
|
600
|
-
lines.append("Assess un-reviewed rules: /loop "
|
|
601
|
-
f"{c.get('not_reviewed', 0) or 1} /stig-assess {parts[0]}")
|
|
602
|
-
self._info("\n".join(lines))
|
|
590
|
+
status = parts[1] if len(parts) > 1 else None
|
|
591
|
+
self._info("\n".join(stig.summary_lines(cl, parts[0], status)))
|
|
603
592
|
|
|
604
593
|
def _cmd_rmf(self, arg: str) -> None:
|
|
605
594
|
"""RMF automation: bootstrap the NIST 800-53 catalog into the knowledge
|
|
@@ -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.73"
|
|
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"
|
|
@@ -55,3 +55,33 @@ def test_bad_server_is_skipped(tmp_path):
|
|
|
55
55
|
schemas = mcp.connect_all(str(tmp_path), log=logs.append)
|
|
56
56
|
assert schemas == []
|
|
57
57
|
assert any("failed to start" in m for m in logs)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_content_to_text_joins_text_blocks():
|
|
61
|
+
out = mcp._content_to_text({"content": [{"type": "text", "text": "line1"},
|
|
62
|
+
{"type": "text", "text": "line2"}]})
|
|
63
|
+
assert out == "line1\nline2"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_content_to_text_non_text_block_is_json():
|
|
67
|
+
out = mcp._content_to_text({"content": [{"type": "image", "data": "abc"}]})
|
|
68
|
+
assert '"type": "image"' in out and '"data": "abc"' in out
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_content_to_text_error_and_empty_and_nondict():
|
|
72
|
+
assert mcp._content_to_text({"content": [{"type": "text", "text": "boom"}],
|
|
73
|
+
"isError": True}).startswith("[tool reported an error]")
|
|
74
|
+
assert mcp._content_to_text({"content": []}) == "(empty result)"
|
|
75
|
+
assert mcp._content_to_text("raw string") == "raw string"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_load_config_project_overrides_user(tmp_path, monkeypatch):
|
|
79
|
+
monkeypatch.setenv("HOME", str(tmp_path))
|
|
80
|
+
user = tmp_path / ".drydock"; user.mkdir()
|
|
81
|
+
(user / "mcp.json").write_text('{"mcpServers": {"db": {"command": "user-cmd"}}}')
|
|
82
|
+
proj = tmp_path / "proj" / ".drydock"; proj.mkdir(parents=True)
|
|
83
|
+
(proj / "mcp.json").write_text('{"mcpServers": {"db": {"command": "proj-cmd"},'
|
|
84
|
+
' "nocmd": {"args": []}}}')
|
|
85
|
+
cfg = mcp.load_config(str(tmp_path / "proj"))
|
|
86
|
+
assert cfg["db"]["command"] == "proj-cmd" # project wins
|
|
87
|
+
assert "nocmd" not in cfg # missing command → skipped
|
|
@@ -60,3 +60,29 @@ def test_graphadd_and_graphquery_tools(tmp_path):
|
|
|
60
60
|
|
|
61
61
|
def test_graphquery_empty_graph_is_graceful(tmp_path):
|
|
62
62
|
assert "empty" in tool_graphquery({"op": "control", "id": "AC-2"}, {"cwd": str(tmp_path)}).lower()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_inherited_controls_handles_cycle():
|
|
66
|
+
"""A RESIDES_ON cycle (misconfigured topology) must not infinite-loop."""
|
|
67
|
+
g = G.RmfGraph()
|
|
68
|
+
a, b = G.component_id("a"), G.component_id("b")
|
|
69
|
+
g.add_node(a, "Component"); g.add_node(b, "Component")
|
|
70
|
+
g.add_edge(a, "RESIDES_ON", b)
|
|
71
|
+
g.add_edge(b, "RESIDES_ON", a) # cycle
|
|
72
|
+
g.add_edge(b, "IMPLEMENTS", G.control_id("pe-3"))
|
|
73
|
+
got = g.inherited_controls(a) # must terminate
|
|
74
|
+
assert G.control_id("pe-3") in got
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_inherited_controls_missing_node_is_empty():
|
|
78
|
+
g = G.RmfGraph()
|
|
79
|
+
assert g.inherited_controls(G.component_id("ghost")) == []
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_neighbors_direction_and_missing():
|
|
83
|
+
g = G.RmfGraph()
|
|
84
|
+
g.add_edge("x", "REL", "y")
|
|
85
|
+
assert g.neighbors("x", "REL") == ["y"]
|
|
86
|
+
assert g.neighbors("y", "REL", direction="in") == ["x"]
|
|
87
|
+
assert g.neighbors("y", "REL", direction="out") == []
|
|
88
|
+
assert g.neighbors("nope") == []
|
|
@@ -93,3 +93,51 @@ def test_empty_and_minimal_checklists(tmp_path):
|
|
|
93
93
|
assert stig.load(tmp_path / "e.ckl").rules == []
|
|
94
94
|
(tmp_path / "m.cklb").write_text('{"stigs":[{"rules":[{"group_id":"V-5"}]}]}')
|
|
95
95
|
assert stig.load(tmp_path / "m.cklb").rules[0].group_id == "V-5"
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_cklb_writeback_only_changes_status_fields(tmp_path):
|
|
99
|
+
"""Editing a .cklb must change ONLY status/finding_details/comments and keep
|
|
100
|
+
everything else byte-stable (eMASS round-trip fidelity)."""
|
|
101
|
+
import json
|
|
102
|
+
data = {"title": "t", "stigs": [{"stig_name": "Demo", "version": "1", "rules": [
|
|
103
|
+
{"group_id": "V-1", "rule_id": "SV-1", "rule_title": "Rule one",
|
|
104
|
+
"severity": "high", "status": "not_reviewed", "check_content": "do x",
|
|
105
|
+
"fix_text": "fix x", "finding_details": "", "comments": "", "weight": "10.0"}]}]}
|
|
106
|
+
p = tmp_path / "a.cklb"; p.write_text(json.dumps(data))
|
|
107
|
+
cl = stig.load(p)
|
|
108
|
+
cl.update("V-1", status="open", finding_details="found it")
|
|
109
|
+
cl.save(p)
|
|
110
|
+
re = json.loads(p.read_text())
|
|
111
|
+
rule = re["stigs"][0]["rules"][0]
|
|
112
|
+
assert rule["status"] == "open" and rule["finding_details"] == "found it"
|
|
113
|
+
# untouched fields preserved exactly
|
|
114
|
+
assert rule["severity"] == "high" and rule["check_content"] == "do x"
|
|
115
|
+
assert rule["weight"] == "10.0" and rule["rule_title"] == "Rule one"
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def test_summary_lines_scale_and_no_silent_truncation(tmp_path):
|
|
119
|
+
import json
|
|
120
|
+
# 60 rules: 55 open, 5 not_reviewed → exercises >40 hint, open hint, >50 cap note
|
|
121
|
+
rules = [{"group_id": f"V-{i}", "rule_id": f"SV-{i}", "rule_title": f"r{i}",
|
|
122
|
+
"severity": "medium", "status": "open" if i < 55 else "not_reviewed"}
|
|
123
|
+
for i in range(60)]
|
|
124
|
+
(tmp_path / "b.cklb").write_text(json.dumps(
|
|
125
|
+
{"stigs": [{"stig_name": "S", "version": "1", "rules": rules}]}))
|
|
126
|
+
cl = stig.load(tmp_path / "b.cklb")
|
|
127
|
+
summ = "\n".join(stig.summary_lines(cl, "b.cklb"))
|
|
128
|
+
assert "/loop 5 /stig-assess b.cklb" in summ # exact not_reviewed count
|
|
129
|
+
assert "List open findings" in summ # open hint shown
|
|
130
|
+
open_view = "\n".join(stig.summary_lines(cl, "b.cklb", "open"))
|
|
131
|
+
assert "showing first 50 of 55" in open_view # no silent truncation
|
|
132
|
+
assert open_view.count("V-") <= 51 # capped list + the note
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def test_summary_lines_large_checklist_hint(tmp_path):
|
|
136
|
+
import json
|
|
137
|
+
rules = [{"group_id": f"V-{i}", "rule_id": f"SV-{i}", "rule_title": "x",
|
|
138
|
+
"severity": "low", "status": "not_reviewed"} for i in range(286)]
|
|
139
|
+
(tmp_path / "big.cklb").write_text(json.dumps(
|
|
140
|
+
{"stigs": [{"stig_name": "ASD", "version": "6", "rules": rules}]}))
|
|
141
|
+
cl = stig.load(tmp_path / "big.cklb")
|
|
142
|
+
summ = "\n".join(stig.summary_lines(cl, "big.cklb"))
|
|
143
|
+
assert "/loop 286" in summ and "286 model turns" in summ
|
|
@@ -70,3 +70,36 @@ def test_search_parses_ddg_html(monkeypatch):
|
|
|
70
70
|
assert res and res[0]["title"] == "Example A"
|
|
71
71
|
assert res[0]["url"] == "https://example.com/a" # uddg redirect unwrapped
|
|
72
72
|
assert res[0]["snippet"] == "snippet A text"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class _FakeResp:
|
|
76
|
+
def __init__(self, body: bytes, ctype: str):
|
|
77
|
+
self._body = body
|
|
78
|
+
self.headers = {"Content-Type": ctype}
|
|
79
|
+
def read(self, *a): return self._body
|
|
80
|
+
def __enter__(self): return self
|
|
81
|
+
def __exit__(self, *a): return False
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_fetch_strips_html_script_and_tags(monkeypatch):
|
|
85
|
+
import urllib.request
|
|
86
|
+
html_doc = (b"<html><head><script>var x=1; evil()</script><style>a{}</style></head>"
|
|
87
|
+
b"<body><h1>Title</h1><p>Hello & welcome</p></body></html>")
|
|
88
|
+
monkeypatch.setattr(urllib.request, "urlopen",
|
|
89
|
+
lambda *a, **k: _FakeResp(html_doc, "text/html"))
|
|
90
|
+
out = web.fetch("example.com")
|
|
91
|
+
assert "Title" in out and "Hello & welcome" in out
|
|
92
|
+
assert "evil()" not in out and "<p>" not in out # script + tags gone
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_fetch_truncates_long_text(monkeypatch):
|
|
96
|
+
import urllib.request
|
|
97
|
+
big = b"<html><body>" + b"x " * 5000 + b"</body></html>"
|
|
98
|
+
monkeypatch.setattr(urllib.request, "urlopen",
|
|
99
|
+
lambda *a, **k: _FakeResp(big, "text/html"))
|
|
100
|
+
out = web.fetch("example.com", max_chars=500)
|
|
101
|
+
assert "truncated" in out and len(out) < 700
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_format_search_empty():
|
|
105
|
+
assert "No web results" in web.format_search("rare query", [])
|
|
@@ -54,3 +54,32 @@ def test_xccdf_to_blank_ckl(tmp_path):
|
|
|
54
54
|
assert stig.load(out).rules[0].status == "open"
|
|
55
55
|
# the regenerated .ckl preserves the DISA status enum
|
|
56
56
|
assert "<STATUS>Open</STATUS>" in out.read_text()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_xccdf_scale_many_rules(tmp_path):
|
|
60
|
+
"""A full DISA STIG (e.g. the ASD STIG) has hundreds of rules. Verify the
|
|
61
|
+
XCCDF->.ckl path handles scale: all rules parsed, well-formed, round-trips,
|
|
62
|
+
full id/title fidelity. (Validated live against U_ASD_STIG_V6R1 = 286 rules.)"""
|
|
63
|
+
rules = "".join(
|
|
64
|
+
f'<Group id="V-{i:05d}"><title>SRG-APP-{i:06d}</title>'
|
|
65
|
+
f'<Rule id="SV-{i:05d}r1_rule" severity="medium" weight="10.0">'
|
|
66
|
+
f'<version>APSC-DV-{i:06d}</version><title>Requirement {i}.</title>'
|
|
67
|
+
f'<description><VulnDiscussion>Discussion {i}.</VulnDiscussion></description>'
|
|
68
|
+
f'<ident system="http://cyber.mil/cci">CCI-{i:06d}</ident>'
|
|
69
|
+
f'<fixtext fixref="F-{i}">Fix {i}.</fixtext>'
|
|
70
|
+
f'<check system="C-{i}"><check-content>Check {i}.</check-content></check>'
|
|
71
|
+
f'</Rule></Group>'
|
|
72
|
+
for i in range(250)
|
|
73
|
+
)
|
|
74
|
+
xccdf = ('<?xml version="1.0"?><Benchmark xmlns="http://checklists.nist.gov/xccdf/1.1" '
|
|
75
|
+
'id="BIG"><title>Big STIG</title><version>1</version>'
|
|
76
|
+
f'<plain-text id="release-info">Release: 1</plain-text>{rules}</Benchmark>')
|
|
77
|
+
p = tmp_path / "big-xccdf.xml"; p.write_text(xccdf)
|
|
78
|
+
cl = stig.xccdf_to_checklist(p, host="srv")
|
|
79
|
+
assert len(cl.rules) == 250
|
|
80
|
+
assert all(r.rule_id and r.title for r in cl.rules) # full fidelity
|
|
81
|
+
out = tmp_path / "big.ckl"; cl.save(out)
|
|
82
|
+
ET.parse(out) # valid at scale
|
|
83
|
+
re = stig.load(out)
|
|
84
|
+
assert len(re.rules) == 250 and re.counts()["not_reviewed"] == 250
|
|
85
|
+
assert re.rules[123].rule_id == "SV-00123r1_rule" # order + content preserved
|
|
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
|