drydock-cli 3.0.64__tar.gz → 3.0.66__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.64/drydock_cli.egg-info → drydock_cli-3.0.66}/PKG-INFO +1 -1
- drydock_cli-3.0.66/drydock/builtin_skills/stig-assess.md +24 -0
- drydock_cli-3.0.66/drydock/stig.py +173 -0
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/drydock/tools/__init__.py +139 -10
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/drydock/tui/app.py +38 -0
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/drydock/tui/widgets.py +2 -1
- {drydock_cli-3.0.64 → drydock_cli-3.0.66/drydock_cli.egg-info}/PKG-INFO +1 -1
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/drydock_cli.egg-info/SOURCES.txt +3 -0
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/pyproject.toml +1 -1
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/tests/test_skills.py +1 -1
- drydock_cli-3.0.66/tests/test_stig.py +70 -0
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/LICENSE +0 -0
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/NOTICE +0 -0
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/README.md +0 -0
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/drydock/__init__.py +0 -0
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/drydock/__main__.py +0 -0
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/drydock/agent.py +0 -0
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/drydock/bash_safety.py +0 -0
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/drydock/builtin_skills/__init__.py +0 -0
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/drydock/builtin_skills/rmf-categorize.md +0 -0
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/drydock/builtin_skills/rmf-control.md +0 -0
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/drydock/builtin_skills/rmf-poam.md +0 -0
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/drydock/builtin_skills/rmf-review.md +0 -0
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/drydock/cli.py +0 -0
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/drydock/compaction.py +0 -0
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/drydock/config.py +0 -0
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/drydock/detect.py +0 -0
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/drydock/extract.py +0 -0
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/drydock/gittools.py +0 -0
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/drydock/graphrag.py +0 -0
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/drydock/guards.py +0 -0
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/drydock/loop_detect.py +0 -0
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/drydock/mcp.py +0 -0
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/drydock/providers.py +0 -0
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/drydock/rmf.py +0 -0
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/drydock/rmf_graph.py +0 -0
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/drydock/skills.py +0 -0
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/drydock/tool_registry.py +0 -0
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/drydock/tui/__init__.py +0 -0
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/drydock/tui/approval.py +0 -0
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/drydock/tui/messages.py +0 -0
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/drydock/tuning.py +0 -0
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/drydock/web.py +0 -0
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/drydock_cli.egg-info/dependency_links.txt +0 -0
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/drydock_cli.egg-info/entry_points.txt +0 -0
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/drydock_cli.egg-info/requires.txt +0 -0
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/drydock_cli.egg-info/top_level.txt +0 -0
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/setup.cfg +0 -0
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/tests/test_approval.py +0 -0
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/tests/test_back_command.py +0 -0
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/tests/test_bash_output_bounding.py +0 -0
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/tests/test_bash_process_group.py +0 -0
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/tests/test_bash_safety.py +0 -0
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/tests/test_bash_timeout_network.py +0 -0
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/tests/test_cli_agents.py +0 -0
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/tests/test_compact_command.py +0 -0
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/tests/test_compaction.py +0 -0
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/tests/test_config.py +0 -0
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/tests/test_config_migration.py +0 -0
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/tests/test_context_limit_config.py +0 -0
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/tests/test_detect.py +0 -0
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/tests/test_dispatch.py +0 -0
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/tests/test_empty_response.py +0 -0
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/tests/test_extract.py +0 -0
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/tests/test_failure_loop.py +0 -0
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/tests/test_first_run_setup.py +0 -0
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/tests/test_gittools.py +0 -0
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/tests/test_graphrag.py +0 -0
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/tests/test_guards_and_tools.py +0 -0
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/tests/test_hallucinated_tools.py +0 -0
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/tests/test_leaked_tool_call.py +0 -0
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/tests/test_loop_detect.py +0 -0
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/tests/test_mcp.py +0 -0
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/tests/test_oneshot_unreachable.py +0 -0
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/tests/test_plan_autocontinue.py +0 -0
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/tests/test_providers_unreachable.py +0 -0
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/tests/test_read_index.py +0 -0
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/tests/test_rmf.py +0 -0
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/tests/test_rmf_graph.py +0 -0
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/tests/test_runaway_repetition.py +0 -0
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/tests/test_stop.py +0 -0
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/tests/test_streaming_newlines.py +0 -0
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/tests/test_subagent.py +0 -0
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/tests/test_system_prompt_help.py +0 -0
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/tests/test_todo.py +0 -0
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/tests/test_tool_arg_parsing.py +0 -0
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/tests/test_tools_undo.py +0 -0
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/tests/test_tui.py +0 -0
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/tests/test_tuning.py +0 -0
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/tests/test_vision_input.py +0 -0
- {drydock_cli-3.0.64 → drydock_cli-3.0.66}/tests/test_web_tools.py +0 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: stig-assess
|
|
3
|
+
description: Assess the next un-reviewed STIG rule against system evidence
|
|
4
|
+
---
|
|
5
|
+
Assess STIG rules in the checklist at: $ARGS
|
|
6
|
+
|
|
7
|
+
Do exactly ONE rule per run (loop it — /loop N /stig-assess <path> — for the
|
|
8
|
+
whole checklist):
|
|
9
|
+
|
|
10
|
+
1. Call StigRules with that path and status=not_reviewed. If none remain, say the
|
|
11
|
+
checklist is fully assessed and STOP without calling other tools.
|
|
12
|
+
2. Take the FIRST not_reviewed rule. Call StigRule (path + rule_id) to read its
|
|
13
|
+
Check Content and Fix Text.
|
|
14
|
+
3. You MUST gather fresh evidence before deciding — do NOT guess. Read the exact
|
|
15
|
+
configuration file the Check Content names (Read tool) or run its check command
|
|
16
|
+
(Bash). Quote the specific line(s) you found. Reason step by step about whether
|
|
17
|
+
that evidence satisfies the Check Content.
|
|
18
|
+
4. Call StigSet (path + rule_id) with:
|
|
19
|
+
- status = not_a_finding ONLY if the evidence clearly shows compliance;
|
|
20
|
+
open if the evidence shows non-compliance;
|
|
21
|
+
not_applicable if the rule does not apply to this system;
|
|
22
|
+
- finding_details = the exact evidence you observed and why it does/doesn't
|
|
23
|
+
comply (quote the config line).
|
|
24
|
+
Give a one-line summary: rule id, the evidence line, and the status you set.
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""STIG checklist engine — parse + generate DISA `.ckl` (XML) and `.cklb` (JSON).
|
|
2
|
+
|
|
3
|
+
These checklists carry hostnames, IPs, and finding statuses that are almost
|
|
4
|
+
always CUI, so all parsing/generation stays local. The model never sees raw XML:
|
|
5
|
+
this exposes a compact per-rule view (id, severity, check content, fix text,
|
|
6
|
+
status) for the LLM to assess one at a time, and writes results back by editing
|
|
7
|
+
the parsed structure IN PLACE — so regenerating produces a well-formed file that
|
|
8
|
+
re-imports into STIG Viewer / eMASS without breaking the schema.
|
|
9
|
+
|
|
10
|
+
Stdlib only (xml.etree + json). All logic original to Drydock.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import xml.etree.ElementTree as ET
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
# Canonical statuses + how each format spells them.
|
|
21
|
+
STATUSES = ("open", "not_a_finding", "not_applicable", "not_reviewed")
|
|
22
|
+
_CKL_OUT = {"open": "Open", "not_a_finding": "NotAFinding",
|
|
23
|
+
"not_applicable": "Not_Applicable", "not_reviewed": "Not_Reviewed"}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def canonical_status(s: str | None) -> str:
|
|
27
|
+
k = (s or "").strip().lower().replace(" ", "_")
|
|
28
|
+
return {
|
|
29
|
+
"open": "open", "notafinding": "not_a_finding", "not_a_finding": "not_a_finding",
|
|
30
|
+
"not_applicable": "not_applicable", "notapplicable": "not_applicable",
|
|
31
|
+
"not_reviewed": "not_reviewed", "notreviewed": "not_reviewed",
|
|
32
|
+
}.get(k, "not_reviewed")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class Rule:
|
|
37
|
+
group_id: str # Vuln_Num, e.g. V-230222
|
|
38
|
+
rule_id: str # SV-230222r...
|
|
39
|
+
title: str
|
|
40
|
+
severity: str # high/medium/low (CAT I/II/III)
|
|
41
|
+
check_content: str
|
|
42
|
+
fix_text: str
|
|
43
|
+
status: str # canonical
|
|
44
|
+
finding_details: str = ""
|
|
45
|
+
comments: str = ""
|
|
46
|
+
_raw: Any = field(default=None, repr=False) # ET element or dict, for edit-in-place
|
|
47
|
+
|
|
48
|
+
def summary(self) -> str:
|
|
49
|
+
return f"{self.group_id} ({self.rule_id}) sev={self.severity} status={self.status} — {self.title}"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class Checklist:
|
|
53
|
+
"""A parsed checklist with edit-in-place fidelity. Format is 'ckl' or 'cklb'."""
|
|
54
|
+
|
|
55
|
+
def __init__(self, fmt: str) -> None:
|
|
56
|
+
self.fmt = fmt
|
|
57
|
+
self.asset: dict = {}
|
|
58
|
+
self.rules: list[Rule] = []
|
|
59
|
+
self._tree: Any = None # ckl: ElementTree
|
|
60
|
+
self._data: Any = None # cklb: dict
|
|
61
|
+
|
|
62
|
+
def by_id(self, ident: str) -> Rule | None:
|
|
63
|
+
ident = ident.strip().lower()
|
|
64
|
+
for r in self.rules:
|
|
65
|
+
if ident in (r.group_id.lower(), r.rule_id.lower()):
|
|
66
|
+
return r
|
|
67
|
+
# tolerate a base rule id without the trailing revision (SV-230222 vs SV-230222r1)
|
|
68
|
+
for r in self.rules:
|
|
69
|
+
if r.rule_id.lower().startswith(ident) or r.group_id.lower().startswith(ident):
|
|
70
|
+
return r
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
def update(self, ident: str, *, status: str | None = None,
|
|
74
|
+
finding_details: str | None = None, comments: str | None = None) -> bool:
|
|
75
|
+
r = self.by_id(ident)
|
|
76
|
+
if r is None:
|
|
77
|
+
return False
|
|
78
|
+
if status is not None:
|
|
79
|
+
r.status = canonical_status(status)
|
|
80
|
+
if finding_details is not None:
|
|
81
|
+
r.finding_details = finding_details
|
|
82
|
+
if comments is not None:
|
|
83
|
+
r.comments = comments
|
|
84
|
+
self._write_back(r)
|
|
85
|
+
return True
|
|
86
|
+
|
|
87
|
+
def counts(self) -> dict:
|
|
88
|
+
c = {s: 0 for s in STATUSES}
|
|
89
|
+
for r in self.rules:
|
|
90
|
+
c[r.status] = c.get(r.status, 0) + 1
|
|
91
|
+
return c
|
|
92
|
+
|
|
93
|
+
# ── per-format write-back (edit the raw structure) ──────────────────────
|
|
94
|
+
def _write_back(self, r: Rule) -> None:
|
|
95
|
+
if self.fmt == "cklb":
|
|
96
|
+
d = r._raw
|
|
97
|
+
d["status"] = r.status
|
|
98
|
+
d["finding_details"] = r.finding_details
|
|
99
|
+
d["comments"] = r.comments
|
|
100
|
+
else: # ckl
|
|
101
|
+
vuln = r._raw
|
|
102
|
+
_set_child_text(vuln, "STATUS", _CKL_OUT[r.status])
|
|
103
|
+
_set_child_text(vuln, "FINDING_DETAILS", r.finding_details)
|
|
104
|
+
_set_child_text(vuln, "COMMENTS", r.comments)
|
|
105
|
+
|
|
106
|
+
def to_text(self) -> str:
|
|
107
|
+
if self.fmt == "cklb":
|
|
108
|
+
return json.dumps(self._data, indent=2)
|
|
109
|
+
ET.indent(self._tree, space=" ")
|
|
110
|
+
return ET.tostring(self._tree.getroot(), encoding="unicode")
|
|
111
|
+
|
|
112
|
+
def save(self, path: str | Path) -> None:
|
|
113
|
+
Path(path).write_text(self.to_text(), encoding="utf-8")
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _set_child_text(el: ET.Element, tag: str, text: str) -> None:
|
|
117
|
+
child = el.find(tag)
|
|
118
|
+
if child is None:
|
|
119
|
+
child = ET.SubElement(el, tag)
|
|
120
|
+
child.text = text or ""
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
# ── parsing ─────────────────────────────────────────────────────────────────
|
|
124
|
+
def parse_ckl(path: str | Path) -> Checklist:
|
|
125
|
+
cl = Checklist("ckl")
|
|
126
|
+
cl._tree = ET.parse(path)
|
|
127
|
+
root = cl._tree.getroot()
|
|
128
|
+
asset = root.find("ASSET")
|
|
129
|
+
if asset is not None:
|
|
130
|
+
cl.asset = {c.tag: (c.text or "") for c in asset}
|
|
131
|
+
for vuln in root.iter("VULN"):
|
|
132
|
+
data: dict[str, str] = {}
|
|
133
|
+
for sd in vuln.findall("STIG_DATA"):
|
|
134
|
+
data[sd.findtext("VULN_ATTRIBUTE") or ""] = sd.findtext("ATTRIBUTE_DATA") or ""
|
|
135
|
+
cl.rules.append(Rule(
|
|
136
|
+
group_id=data.get("Vuln_Num", ""), rule_id=data.get("Rule_ID", ""),
|
|
137
|
+
title=data.get("Rule_Title", ""), severity=data.get("Severity", ""),
|
|
138
|
+
check_content=data.get("Check_Content", ""), fix_text=data.get("Fix_Text", ""),
|
|
139
|
+
status=canonical_status(vuln.findtext("STATUS")),
|
|
140
|
+
finding_details=vuln.findtext("FINDING_DETAILS") or "",
|
|
141
|
+
comments=vuln.findtext("COMMENTS") or "", _raw=vuln,
|
|
142
|
+
))
|
|
143
|
+
return cl
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def parse_cklb(path: str | Path) -> Checklist:
|
|
147
|
+
cl = Checklist("cklb")
|
|
148
|
+
cl._data = json.loads(Path(path).read_text("utf-8"))
|
|
149
|
+
cl.asset = cl._data.get("target_data", {}) or {}
|
|
150
|
+
for stig in cl._data.get("stigs", []) or []:
|
|
151
|
+
for rule in stig.get("rules", []) or []:
|
|
152
|
+
cl.rules.append(Rule(
|
|
153
|
+
group_id=rule.get("group_id", ""), rule_id=rule.get("rule_id", ""),
|
|
154
|
+
title=rule.get("rule_title", rule.get("group_title", "")),
|
|
155
|
+
severity=rule.get("severity", ""),
|
|
156
|
+
check_content=rule.get("check_content", ""), fix_text=rule.get("fix_text", ""),
|
|
157
|
+
status=canonical_status(rule.get("status")),
|
|
158
|
+
finding_details=rule.get("finding_details", ""),
|
|
159
|
+
comments=rule.get("comments", ""), _raw=rule,
|
|
160
|
+
))
|
|
161
|
+
return cl
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def load(path: str | Path) -> Checklist:
|
|
165
|
+
"""Parse a .ckl (XML) or .cklb (JSON) checklist."""
|
|
166
|
+
ext = Path(path).suffix.lower()
|
|
167
|
+
if ext == ".cklb":
|
|
168
|
+
return parse_cklb(path)
|
|
169
|
+
if ext == ".ckl":
|
|
170
|
+
return parse_ckl(path)
|
|
171
|
+
# sniff: JSON → cklb, else ckl
|
|
172
|
+
head = Path(path).read_text("utf-8", "ignore").lstrip()[:1]
|
|
173
|
+
return parse_cklb(path) if head == "{" else parse_ckl(path)
|
|
@@ -270,6 +270,58 @@ SCHEMAS = [
|
|
|
270
270
|
"required": ["message"],
|
|
271
271
|
},
|
|
272
272
|
},
|
|
273
|
+
{
|
|
274
|
+
"name": "StigRules",
|
|
275
|
+
"description": (
|
|
276
|
+
"List the rules in a DISA STIG checklist (.ckl / .cklb) with their "
|
|
277
|
+
"status, optionally filtered by status (e.g. not_reviewed). Use it to "
|
|
278
|
+
"see what needs assessing. Pair with StigRule to read one, StigSet to "
|
|
279
|
+
"record a result."
|
|
280
|
+
),
|
|
281
|
+
"input_schema": {
|
|
282
|
+
"type": "object",
|
|
283
|
+
"properties": {
|
|
284
|
+
"path": {"type": "string", "description": "Path to the .ckl/.cklb file."},
|
|
285
|
+
"status": {"type": "string", "description": "Optional filter: open|not_a_finding|not_applicable|not_reviewed"},
|
|
286
|
+
},
|
|
287
|
+
"required": ["path"],
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
"name": "StigRule",
|
|
292
|
+
"description": (
|
|
293
|
+
"Read ONE STIG rule's full detail — Check Content + Fix Text — so you "
|
|
294
|
+
"can assess it against system evidence. Assess one rule at a time."
|
|
295
|
+
),
|
|
296
|
+
"input_schema": {
|
|
297
|
+
"type": "object",
|
|
298
|
+
"properties": {
|
|
299
|
+
"path": {"type": "string"},
|
|
300
|
+
"rule_id": {"type": "string", "description": "Vuln_Num (V-…) or Rule_ID (SV-…)."},
|
|
301
|
+
},
|
|
302
|
+
"required": ["path", "rule_id"],
|
|
303
|
+
},
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
"name": "StigSet",
|
|
307
|
+
"description": (
|
|
308
|
+
"Record an assessment result for a STIG rule and save the checklist: "
|
|
309
|
+
"set status (open / not_a_finding / not_applicable / not_reviewed) and "
|
|
310
|
+
"the finding_details / comments narrative. Regenerates a valid "
|
|
311
|
+
".ckl/.cklb that re-imports into STIG Viewer / eMASS."
|
|
312
|
+
),
|
|
313
|
+
"input_schema": {
|
|
314
|
+
"type": "object",
|
|
315
|
+
"properties": {
|
|
316
|
+
"path": {"type": "string"},
|
|
317
|
+
"rule_id": {"type": "string"},
|
|
318
|
+
"status": {"type": "string", "description": "open | not_a_finding | not_applicable | not_reviewed"},
|
|
319
|
+
"finding_details": {"type": "string"},
|
|
320
|
+
"comments": {"type": "string"},
|
|
321
|
+
},
|
|
322
|
+
"required": ["path", "rule_id"],
|
|
323
|
+
},
|
|
324
|
+
},
|
|
273
325
|
{
|
|
274
326
|
"name": "GraphQuery",
|
|
275
327
|
"description": (
|
|
@@ -1102,6 +1154,78 @@ def _rmf_graph(config):
|
|
|
1102
1154
|
return rmf_graph, rmf_graph.RmfGraph.load(path), path
|
|
1103
1155
|
|
|
1104
1156
|
|
|
1157
|
+
def _gattrs(g, nid) -> dict:
|
|
1158
|
+
n = g.get(nid)
|
|
1159
|
+
return n["attrs"] if n else {}
|
|
1160
|
+
|
|
1161
|
+
|
|
1162
|
+
def _stig_path(params, config):
|
|
1163
|
+
p = (params.get("path") or "").strip()
|
|
1164
|
+
return _resolve_path(p, config) if p else None
|
|
1165
|
+
|
|
1166
|
+
|
|
1167
|
+
def tool_stigrules(params: dict, config: dict) -> str:
|
|
1168
|
+
"""List the rules in a STIG checklist (.ckl/.cklb), optionally by status."""
|
|
1169
|
+
from drydock import stig
|
|
1170
|
+
path = _stig_path(params, config)
|
|
1171
|
+
if not path:
|
|
1172
|
+
return "Error: StigRules needs a `path` to a .ckl/.cklb checklist."
|
|
1173
|
+
try:
|
|
1174
|
+
cl = stig.load(path)
|
|
1175
|
+
except Exception as e: # noqa: BLE001
|
|
1176
|
+
return f"Could not read checklist: {e}"
|
|
1177
|
+
status = params.get("status")
|
|
1178
|
+
sf = stig.canonical_status(status) if status else None
|
|
1179
|
+
rules = [r for r in cl.rules if sf is None or r.status == sf]
|
|
1180
|
+
head = (f"{path}: {len(cl.rules)} rules — " +
|
|
1181
|
+
", ".join(f"{k}={v}" for k, v in cl.counts().items()))
|
|
1182
|
+
lines = [head] + [f" {r.summary()}" for r in rules[:400]]
|
|
1183
|
+
if len(rules) > 400:
|
|
1184
|
+
lines.append(f" … +{len(rules) - 400} more")
|
|
1185
|
+
return "\n".join(lines)
|
|
1186
|
+
|
|
1187
|
+
|
|
1188
|
+
def tool_stigrule(params: dict, config: dict) -> str:
|
|
1189
|
+
"""Full detail of ONE STIG rule (check content + fix text) for assessment."""
|
|
1190
|
+
from drydock import stig
|
|
1191
|
+
path = _stig_path(params, config)
|
|
1192
|
+
rid = (params.get("rule_id") or "").strip()
|
|
1193
|
+
if not path or not rid:
|
|
1194
|
+
return "Error: StigRule needs `path` and `rule_id`."
|
|
1195
|
+
try:
|
|
1196
|
+
cl = stig.load(path)
|
|
1197
|
+
except Exception as e: # noqa: BLE001
|
|
1198
|
+
return f"Could not read checklist: {e}"
|
|
1199
|
+
r = cl.by_id(rid)
|
|
1200
|
+
if r is None:
|
|
1201
|
+
return f"No rule {rid} in {path}."
|
|
1202
|
+
return (f"{r.group_id} ({r.rule_id}) — {r.title}\nSeverity: {r.severity}\n"
|
|
1203
|
+
f"Status: {r.status}\nCheck Content:\n{r.check_content}\n\n"
|
|
1204
|
+
f"Fix Text:\n{r.fix_text}")
|
|
1205
|
+
|
|
1206
|
+
|
|
1207
|
+
def tool_stigset(params: dict, config: dict) -> str:
|
|
1208
|
+
"""Set a STIG rule's status + finding details/comments and save the file."""
|
|
1209
|
+
from drydock import stig
|
|
1210
|
+
path = _stig_path(params, config)
|
|
1211
|
+
rid = (params.get("rule_id") or "").strip()
|
|
1212
|
+
if not path or not rid:
|
|
1213
|
+
return "Error: StigSet needs `path` and `rule_id`."
|
|
1214
|
+
status = params.get("status")
|
|
1215
|
+
if status and stig.canonical_status(status) not in stig.STATUSES:
|
|
1216
|
+
return f"status must be one of {stig.STATUSES}."
|
|
1217
|
+
try:
|
|
1218
|
+
cl = stig.load(path)
|
|
1219
|
+
ok = cl.update(rid, status=status, finding_details=params.get("finding_details"),
|
|
1220
|
+
comments=params.get("comments"))
|
|
1221
|
+
if not ok:
|
|
1222
|
+
return f"No rule {rid} in {path}."
|
|
1223
|
+
cl.save(path)
|
|
1224
|
+
except Exception as e: # noqa: BLE001
|
|
1225
|
+
return f"Could not update checklist: {e}"
|
|
1226
|
+
return f"✓ {rid} set to {stig.canonical_status(status) if status else 'unchanged'} in {path}."
|
|
1227
|
+
|
|
1228
|
+
|
|
1105
1229
|
def tool_graphquery(params: dict, config: dict) -> str:
|
|
1106
1230
|
"""Traverse the RMF typed ontology graph (read-only)."""
|
|
1107
1231
|
rmf_graph, g, _ = _rmf_graph(config)
|
|
@@ -1115,8 +1239,8 @@ def tool_graphquery(params: dict, config: dict) -> str:
|
|
|
1115
1239
|
n = g.get(node)
|
|
1116
1240
|
if not n:
|
|
1117
1241
|
return f"No control {ident} in the graph."
|
|
1118
|
-
objs = [g
|
|
1119
|
-
impl = [g
|
|
1242
|
+
objs = [_gattrs(g, o).get("prose", "") for o in g.neighbors(node, "ASSESSES", direction="in")]
|
|
1243
|
+
impl = [_gattrs(g, c).get("name", c) for c in g.neighbors(node, "IMPLEMENTS", direction="in")]
|
|
1120
1244
|
out = [f"{n['attrs'].get('control_id', ident)} — {n['attrs'].get('title','')} "
|
|
1121
1245
|
f"(Family: {n['attrs'].get('family','')})"]
|
|
1122
1246
|
if objs:
|
|
@@ -1124,28 +1248,28 @@ def tool_graphquery(params: dict, config: dict) -> str:
|
|
|
1124
1248
|
out.append("Implemented by: " + (", ".join(impl) if impl else "(no components recorded)"))
|
|
1125
1249
|
return "\n".join(out)
|
|
1126
1250
|
if op == "family":
|
|
1127
|
-
ctrls = [g
|
|
1251
|
+
ctrls = [_gattrs(g, c).get("control_id", c)
|
|
1128
1252
|
for c in g.of_type("Control")
|
|
1129
|
-
if g
|
|
1130
|
-
or ident.lower() in g
|
|
1253
|
+
if _gattrs(g, c).get("family", "").lower().startswith(ident.lower())
|
|
1254
|
+
or ident.lower() in _gattrs(g, c).get("family", "").lower()]
|
|
1131
1255
|
return f"Controls in '{ident}': " + (", ".join(sorted(ctrls)) or "(none)")
|
|
1132
1256
|
if op in ("component", "implementers", "inherited"):
|
|
1133
1257
|
comp = rmf_graph.component_id(ident)
|
|
1134
1258
|
if op == "implementers":
|
|
1135
1259
|
node = rmf_graph.control_id(ident)
|
|
1136
|
-
comps = [g
|
|
1260
|
+
comps = [_gattrs(g, c).get("name", c) for c in g.neighbors(node, "IMPLEMENTS", direction="in")]
|
|
1137
1261
|
return f"Components implementing {ident}: " + (", ".join(comps) or "(none)")
|
|
1138
1262
|
if op == "inherited":
|
|
1139
1263
|
inh = g.inherited_controls(comp)
|
|
1140
|
-
names = [g
|
|
1264
|
+
names = [_gattrs(g, c).get("control_id", c) if g.get(c) else c for c in inh]
|
|
1141
1265
|
return (f"{ident} inherits {len(names)} control(s) from its parent system(s): "
|
|
1142
1266
|
+ (", ".join(names) or "(none — no RESIDES_ON recorded)"))
|
|
1143
1267
|
n = g.get(comp)
|
|
1144
1268
|
if not n:
|
|
1145
1269
|
return f"No component '{ident}' in the graph (add it with GraphAdd)."
|
|
1146
|
-
impl = [g
|
|
1147
|
-
res = [g
|
|
1148
|
-
vulns = [g
|
|
1270
|
+
impl = [_gattrs(g, c).get("control_id", c) for c in g.neighbors(comp, "IMPLEMENTS", direction="out")]
|
|
1271
|
+
res = [_gattrs(g, p).get("name", p) for p in g.neighbors(comp, "RESIDES_ON", direction="out")]
|
|
1272
|
+
vulns = [_gattrs(g, v).get("vuln_id", v) for v in g.neighbors(comp, "AFFECTS", direction="in")]
|
|
1149
1273
|
return (f"Component {ident} ({n['attrs'].get('os','')}): implements "
|
|
1150
1274
|
f"{', '.join(impl) or 'none'}; resides on {', '.join(res) or 'nothing'}; "
|
|
1151
1275
|
f"flaws {', '.join(vulns) or 'none'}.")
|
|
@@ -1258,6 +1382,9 @@ _TOOLS = [
|
|
|
1258
1382
|
("Knowledge", tool_knowledge, True),
|
|
1259
1383
|
("GraphQuery", tool_graphquery, True),
|
|
1260
1384
|
("GraphAdd", tool_graphadd, False),
|
|
1385
|
+
("StigRules", tool_stigrules, True),
|
|
1386
|
+
("StigRule", tool_stigrule, True),
|
|
1387
|
+
("StigSet", tool_stigset, False),
|
|
1261
1388
|
("WebSearch", tool_websearch, True),
|
|
1262
1389
|
("WebFetch", tool_webfetch, True),
|
|
1263
1390
|
("GitStatus", tool_gitstatus, True),
|
|
@@ -1275,6 +1402,7 @@ def register_all():
|
|
|
1275
1402
|
"todo": tool_todo, "task": tool_task, "Dispatch": tool_dispatch,
|
|
1276
1403
|
"Knowledge": tool_knowledge,
|
|
1277
1404
|
"GraphQuery": tool_graphquery, "GraphAdd": tool_graphadd,
|
|
1405
|
+
"StigRules": tool_stigrules, "StigRule": tool_stigrule, "StigSet": tool_stigset,
|
|
1278
1406
|
"WebSearch": tool_websearch, "WebFetch": tool_webfetch,
|
|
1279
1407
|
"GitStatus": tool_gitstatus, "GitDiff": tool_gitdiff,
|
|
1280
1408
|
"GitLog": tool_gitlog, "GitCommit": tool_gitcommit,
|
|
@@ -1283,6 +1411,7 @@ def register_all():
|
|
|
1283
1411
|
# GitCommit + GraphAdd write).
|
|
1284
1412
|
read_only = name in (
|
|
1285
1413
|
"Read", "Glob", "Grep", "task", "Dispatch", "Knowledge", "GraphQuery",
|
|
1414
|
+
"StigRules", "StigRule",
|
|
1286
1415
|
"WebSearch", "WebFetch", "GitStatus", "GitDiff", "GitLog",
|
|
1287
1416
|
)
|
|
1288
1417
|
register(ToolDef(name=name, schema=schema, func=func, read_only=read_only))
|
|
@@ -496,6 +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 summarize a .ckl/.cklb; assess via /stig-assess\n"
|
|
499
500
|
" /clear reset the conversation\n"
|
|
500
501
|
" /quit exit\n"
|
|
501
502
|
"Type a task and press Enter. ↑/↓ recall history · Esc stops · Ctrl+O expands tools."
|
|
@@ -504,6 +505,8 @@ class DrydockApp(App):
|
|
|
504
505
|
self._cmd_mcp()
|
|
505
506
|
elif cmd == "/rmf":
|
|
506
507
|
self._cmd_rmf(arg)
|
|
508
|
+
elif cmd == "/stig":
|
|
509
|
+
self._cmd_stig(arg)
|
|
507
510
|
elif cmd == "/loop":
|
|
508
511
|
self._cmd_loop(arg)
|
|
509
512
|
elif cmd == "/skills":
|
|
@@ -513,6 +516,41 @@ class DrydockApp(App):
|
|
|
513
516
|
else:
|
|
514
517
|
self._mount(ErrorMessage(f"unknown command: {cmd} (try /help)"))
|
|
515
518
|
|
|
519
|
+
def _cmd_stig(self, arg: str) -> None:
|
|
520
|
+
"""Summarize a STIG checklist (.ckl/.cklb): asset + status counts, or list
|
|
521
|
+
rules of a given status. Assess it with /stig-assess (loop for the whole
|
|
522
|
+
checklist)."""
|
|
523
|
+
from drydock import stig
|
|
524
|
+
|
|
525
|
+
parts = arg.split()
|
|
526
|
+
if not parts:
|
|
527
|
+
self._info("usage: /stig <path-to.ckl|.cklb> [status] "
|
|
528
|
+
"(status: open|not_a_finding|not_applicable|not_reviewed)\n"
|
|
529
|
+
"Assess it: /loop <n> /stig-assess <path>")
|
|
530
|
+
return
|
|
531
|
+
import os as _os
|
|
532
|
+
path = parts[0]
|
|
533
|
+
if not _os.path.isabs(path):
|
|
534
|
+
path = _os.path.join(self.config.get("cwd") or ".", path)
|
|
535
|
+
try:
|
|
536
|
+
cl = stig.load(path)
|
|
537
|
+
except Exception as e: # noqa: BLE001
|
|
538
|
+
self._mount(ErrorMessage(f"could not read checklist: {e}"))
|
|
539
|
+
return
|
|
540
|
+
host = cl.asset.get("HOST_NAME") or cl.asset.get("host_name") or "?"
|
|
541
|
+
c = cl.counts()
|
|
542
|
+
lines = [f"STIG checklist {path} (host: {host}, format: {cl.fmt})",
|
|
543
|
+
f" {len(cl.rules)} rules — " + " · ".join(f"{k}={v}" for k, v in c.items())]
|
|
544
|
+
if len(parts) > 1:
|
|
545
|
+
sf = stig.canonical_status(parts[1])
|
|
546
|
+
hits = [r for r in cl.rules if r.status == sf]
|
|
547
|
+
lines.append(f"\n{sf} ({len(hits)}):")
|
|
548
|
+
lines += [f" {r.group_id} ({r.severity}) — {r.title}" for r in hits[:50]]
|
|
549
|
+
else:
|
|
550
|
+
lines.append("Assess un-reviewed rules: /loop "
|
|
551
|
+
f"{c.get('not_reviewed', 0) or 1} /stig-assess {parts[0]}")
|
|
552
|
+
self._info("\n".join(lines))
|
|
553
|
+
|
|
516
554
|
def _cmd_rmf(self, arg: str) -> None:
|
|
517
555
|
"""RMF automation: bootstrap the NIST 800-53 catalog into the knowledge
|
|
518
556
|
base and surface the bundled RMF skills."""
|
|
@@ -153,7 +153,8 @@ class PromptHistory:
|
|
|
153
153
|
# completes the prefix.
|
|
154
154
|
SLASH_COMMANDS = [
|
|
155
155
|
"/help", "/model", "/cwd", "/undo", "/back", "/stop", "/status",
|
|
156
|
-
"/compact", "/graphrag", "/skills", "/loop", "/mcp", "/rmf", "/
|
|
156
|
+
"/compact", "/graphrag", "/skills", "/loop", "/mcp", "/rmf", "/stig",
|
|
157
|
+
"/clear", "/quit",
|
|
157
158
|
]
|
|
158
159
|
|
|
159
160
|
|
|
@@ -20,6 +20,7 @@ drydock/providers.py
|
|
|
20
20
|
drydock/rmf.py
|
|
21
21
|
drydock/rmf_graph.py
|
|
22
22
|
drydock/skills.py
|
|
23
|
+
drydock/stig.py
|
|
23
24
|
drydock/tool_registry.py
|
|
24
25
|
drydock/tuning.py
|
|
25
26
|
drydock/web.py
|
|
@@ -28,6 +29,7 @@ drydock/builtin_skills/rmf-categorize.md
|
|
|
28
29
|
drydock/builtin_skills/rmf-control.md
|
|
29
30
|
drydock/builtin_skills/rmf-poam.md
|
|
30
31
|
drydock/builtin_skills/rmf-review.md
|
|
32
|
+
drydock/builtin_skills/stig-assess.md
|
|
31
33
|
drydock/tools/__init__.py
|
|
32
34
|
drydock/tui/__init__.py
|
|
33
35
|
drydock/tui/app.py
|
|
@@ -73,6 +75,7 @@ tests/test_rmf.py
|
|
|
73
75
|
tests/test_rmf_graph.py
|
|
74
76
|
tests/test_runaway_repetition.py
|
|
75
77
|
tests/test_skills.py
|
|
78
|
+
tests/test_stig.py
|
|
76
79
|
tests/test_stop.py
|
|
77
80
|
tests/test_streaming_newlines.py
|
|
78
81
|
tests/test_subagent.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.
|
|
10
|
+
version = "3.0.66"
|
|
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,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"}
|
|
58
|
+
assert set(loaded) == {"rmf-control", "rmf-categorize", "rmf-review", "rmf-poam", "stig-assess"}
|
|
59
59
|
|
|
60
60
|
|
|
61
61
|
def test_create_skill_user_scope(tmp_path, monkeypatch):
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""STIG .ckl/.cklb engine — parse, edit-in-place, round-trip; + the tools."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
|
|
6
|
+
from drydock import stig
|
|
7
|
+
from drydock.tools import tool_stigrules, tool_stigrule, tool_stigset
|
|
8
|
+
|
|
9
|
+
_CKL = '''<?xml version="1.0" encoding="UTF-8"?>
|
|
10
|
+
<CHECKLIST><ASSET><HOST_NAME>web01</HOST_NAME><HOST_IP>10.0.0.5</HOST_IP></ASSET>
|
|
11
|
+
<STIGS><iSTIG><STIG_INFO></STIG_INFO>
|
|
12
|
+
<VULN>
|
|
13
|
+
<STIG_DATA><VULN_ATTRIBUTE>Vuln_Num</VULN_ATTRIBUTE><ATTRIBUTE_DATA>V-230222</ATTRIBUTE_DATA></STIG_DATA>
|
|
14
|
+
<STIG_DATA><VULN_ATTRIBUTE>Rule_ID</VULN_ATTRIBUTE><ATTRIBUTE_DATA>SV-230222r1</ATTRIBUTE_DATA></STIG_DATA>
|
|
15
|
+
<STIG_DATA><VULN_ATTRIBUTE>Severity</VULN_ATTRIBUTE><ATTRIBUTE_DATA>medium</ATTRIBUTE_DATA></STIG_DATA>
|
|
16
|
+
<STIG_DATA><VULN_ATTRIBUTE>Rule_Title</VULN_ATTRIBUTE><ATTRIBUTE_DATA>Enable FIPS</ATTRIBUTE_DATA></STIG_DATA>
|
|
17
|
+
<STIG_DATA><VULN_ATTRIBUTE>Check_Content</VULN_ATTRIBUTE><ATTRIBUTE_DATA>Run fips-mode-setup --check</ATTRIBUTE_DATA></STIG_DATA>
|
|
18
|
+
<STIG_DATA><VULN_ATTRIBUTE>Fix_Text</VULN_ATTRIBUTE><ATTRIBUTE_DATA>Run fips-mode-setup --enable</ATTRIBUTE_DATA></STIG_DATA>
|
|
19
|
+
<STATUS>Not_Reviewed</STATUS><FINDING_DETAILS></FINDING_DETAILS><COMMENTS></COMMENTS></VULN>
|
|
20
|
+
</iSTIG></STIGS></CHECKLIST>'''
|
|
21
|
+
|
|
22
|
+
_CKLB = {"title": "RHEL9", "target_data": {"host_name": "web01"}, "stigs": [{"stig_name": "RHEL 9",
|
|
23
|
+
"rules": [{"group_id": "V-230222", "rule_id": "SV-230222r1", "rule_title": "Enable FIPS",
|
|
24
|
+
"severity": "medium", "check_content": "fips-mode-setup --check",
|
|
25
|
+
"fix_text": "fips-mode-setup --enable", "status": "not_reviewed",
|
|
26
|
+
"finding_details": "", "comments": ""}]}]}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_ckl_roundtrip_edit_in_place(tmp_path):
|
|
30
|
+
p = tmp_path / "r.ckl"; p.write_text(_CKL)
|
|
31
|
+
cl = stig.load(p)
|
|
32
|
+
assert cl.fmt == "ckl" and cl.asset["HOST_NAME"] == "web01"
|
|
33
|
+
assert cl.rules[0].check_content == "Run fips-mode-setup --check"
|
|
34
|
+
assert cl.update("V-230222", status="open", finding_details="FIPS off.", comments="by drydock")
|
|
35
|
+
cl.save(p)
|
|
36
|
+
re = stig.load(p)
|
|
37
|
+
assert re.rules[0].status == "open" and re.rules[0].finding_details == "FIPS off."
|
|
38
|
+
# other STIG_DATA preserved (schema fidelity)
|
|
39
|
+
assert "Vuln_Num" in p.read_text() and "<STATUS>Open</STATUS>" in p.read_text()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_cklb_roundtrip(tmp_path):
|
|
43
|
+
p = tmp_path / "r.cklb"; p.write_text(json.dumps(_CKLB))
|
|
44
|
+
cl = stig.load(p)
|
|
45
|
+
assert cl.fmt == "cklb"
|
|
46
|
+
cl.update("SV-230222r1", status="not_a_finding", finding_details="FIPS on.")
|
|
47
|
+
cl.save(p)
|
|
48
|
+
data = json.loads(p.read_text()) # still valid JSON
|
|
49
|
+
assert data["stigs"][0]["rules"][0]["status"] == "not_a_finding"
|
|
50
|
+
assert stig.load(p).rules[0].finding_details == "FIPS on."
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_status_canonicalization():
|
|
54
|
+
assert stig.canonical_status("NotAFinding") == "not_a_finding"
|
|
55
|
+
assert stig.canonical_status("Not_Applicable") == "not_applicable"
|
|
56
|
+
assert stig.canonical_status("garbage") == "not_reviewed"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_stig_tools(tmp_path):
|
|
60
|
+
p = tmp_path / "r.ckl"; p.write_text(_CKL); cfg = {"cwd": str(tmp_path)}
|
|
61
|
+
out = tool_stigrules({"path": "r.ckl"}, cfg)
|
|
62
|
+
assert "V-230222" in out and "not_reviewed=1" in out
|
|
63
|
+
detail = tool_stigrule({"path": "r.ckl", "rule_id": "V-230222"}, cfg)
|
|
64
|
+
assert "Check Content" in detail and "fips-mode-setup --check" in detail
|
|
65
|
+
set_out = tool_stigset({"path": "r.ckl", "rule_id": "V-230222", "status": "open",
|
|
66
|
+
"finding_details": "FIPS disabled."}, cfg)
|
|
67
|
+
assert "set to open" in set_out
|
|
68
|
+
assert stig.load(p).rules[0].status == "open"
|
|
69
|
+
# filter by status
|
|
70
|
+
assert "not_reviewed=0" in tool_stigrules({"path": "r.ckl"}, cfg)
|
|
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
|