drydock-cli 3.0.73__tar.gz → 3.0.75__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.73/drydock_cli.egg-info → drydock_cli-3.0.75}/PKG-INFO +37 -10
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/README.md +36 -9
- drydock_cli-3.0.75/drydock/cci.py +109 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/drydock/rmf_graph.py +13 -5
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/drydock/tools/__init__.py +23 -4
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/drydock/tui/app.py +27 -14
- {drydock_cli-3.0.73 → drydock_cli-3.0.75/drydock_cli.egg-info}/PKG-INFO +37 -10
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/drydock_cli.egg-info/SOURCES.txt +2 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/pyproject.toml +1 -1
- drydock_cli-3.0.75/tests/test_cci.py +82 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/tests/test_dispatch.py +11 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/LICENSE +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/NOTICE +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/drydock/__init__.py +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/drydock/__main__.py +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/drydock/agent.py +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/drydock/bash_safety.py +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/drydock/builtin_skills/__init__.py +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/drydock/builtin_skills/rmf-categorize.md +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/drydock/builtin_skills/rmf-control.md +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/drydock/builtin_skills/rmf-poam.md +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/drydock/builtin_skills/rmf-review.md +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/drydock/builtin_skills/stig-assess.md +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/drydock/builtin_skills/stig-remediate.md +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/drydock/cli.py +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/drydock/compaction.py +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/drydock/config.py +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/drydock/detect.py +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/drydock/extract.py +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/drydock/gittools.py +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/drydock/graphrag.py +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/drydock/guards.py +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/drydock/loop_detect.py +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/drydock/mcp.py +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/drydock/providers.py +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/drydock/rmf.py +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/drydock/skills.py +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/drydock/stig.py +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/drydock/tool_registry.py +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/drydock/tui/__init__.py +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/drydock/tui/approval.py +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/drydock/tui/messages.py +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/drydock/tui/widgets.py +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/drydock/tuning.py +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/drydock/web.py +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/drydock_cli.egg-info/dependency_links.txt +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/drydock_cli.egg-info/entry_points.txt +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/drydock_cli.egg-info/requires.txt +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/drydock_cli.egg-info/top_level.txt +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/setup.cfg +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/tests/test_approval.py +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/tests/test_back_command.py +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/tests/test_bash_output_bounding.py +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/tests/test_bash_process_group.py +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/tests/test_bash_safety.py +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/tests/test_bash_timeout_network.py +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/tests/test_cli_agents.py +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/tests/test_compact_command.py +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/tests/test_compaction.py +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/tests/test_config.py +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/tests/test_config_migration.py +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/tests/test_context_limit_config.py +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/tests/test_detect.py +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/tests/test_e2e_connected.py +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/tests/test_empty_response.py +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/tests/test_extract.py +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/tests/test_failure_loop.py +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/tests/test_first_run_setup.py +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/tests/test_gittools.py +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/tests/test_graphrag.py +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/tests/test_guards_and_tools.py +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/tests/test_hallucinated_tools.py +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/tests/test_leaked_tool_call.py +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/tests/test_loop_detect.py +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/tests/test_mcp.py +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/tests/test_oneshot_unreachable.py +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/tests/test_plan_autocontinue.py +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/tests/test_providers_unreachable.py +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/tests/test_read_index.py +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/tests/test_rmf.py +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/tests/test_rmf_graph.py +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/tests/test_rmf_stig_graph.py +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/tests/test_runaway_repetition.py +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/tests/test_skills.py +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/tests/test_stig.py +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/tests/test_stop.py +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/tests/test_streaming_newlines.py +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/tests/test_subagent.py +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/tests/test_system_prompt_help.py +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/tests/test_todo.py +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/tests/test_tool_arg_parsing.py +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/tests/test_tools_undo.py +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/tests/test_tui.py +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/tests/test_tuning.py +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/tests/test_vision_input.py +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/tests/test_web_tools.py +0 -0
- {drydock_cli-3.0.73 → drydock_cli-3.0.75}/tests/test_xccdf.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: drydock-cli
|
|
3
|
-
Version: 3.0.
|
|
3
|
+
Version: 3.0.75
|
|
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
|
|
@@ -91,10 +91,14 @@ Typed into the prompt. The agent also knows these, so you can just **ask it**
|
|
|
91
91
|
| `/mcp` | List connected MCP servers + their tools |
|
|
92
92
|
| `/rmf bootstrap [families]` | Ingest the NIST SP 800-53 catalog (RMF automation) |
|
|
93
93
|
| `/rmf-control` · `/rmf-categorize` · `/rmf-review` · `/rmf-poam` | Bundled RMF skills |
|
|
94
|
+
| `/stig new <xccdf>` | Generate a blank `.ckl` from a DISA STIG benchmark |
|
|
95
|
+
| `/stig <ckl>` · `/stig <ckl> open` | Summarize a checklist · list findings by status |
|
|
96
|
+
| `/stig graph <ckl>` | Ingest a checklist into the RMF graph (auto-links rules→controls via CCI) |
|
|
97
|
+
| `/stig-assess <ckl>` · `/stig-remediate <ckl> <rule>` | Assess a rule vs evidence · write a fix script |
|
|
94
98
|
| `/model` · `/cwd` | Show/set model & endpoint · working directory |
|
|
95
99
|
| `/undo` · `/back` | Revert the last write · rewind the last turn |
|
|
96
|
-
| `/compact` · `/
|
|
97
|
-
| `/help` · `/quit` |
|
|
100
|
+
| `/compact` · `/context [n]` | Shrink context now · view/set the context-window budget |
|
|
101
|
+
| `/status` · `/clear` · `/help` · `/quit` | Session stats · reset · help · exit |
|
|
98
102
|
|
|
99
103
|
### Knowledge base (GraphRAG) — ingesting your documents
|
|
100
104
|
|
|
@@ -107,9 +111,11 @@ Typed into the prompt. The agent also knows these, so you can just **ask it**
|
|
|
107
111
|
|
|
108
112
|
Once built, the agent **automatically** retrieves from it (read-only `Knowledge`
|
|
109
113
|
tool) when a question touches your material. Ingests text formats
|
|
110
|
-
(`.md .txt .py .js .json .yaml .sql …`) **
|
|
111
|
-
|
|
112
|
-
|
|
114
|
+
(`.md .txt .py .js .json .yaml .sql …`), **PDF and Word (`.docx`)**, and **STIG
|
|
115
|
+
checklists (`.ckl`/`.cklb`)** — checklists are flattened to per-rule findings so
|
|
116
|
+
you can ask "which findings are open?". `.docx`/`.ckl` need nothing extra; PDF
|
|
117
|
+
uses the `pdftotext` binary (poppler) if present, else `pip install
|
|
118
|
+
drydock-cli[pdf]` (pypdf). The index is a single JSON at
|
|
113
119
|
`<project>/.drydock/graphrag.json` — clean-room, no embeddings.
|
|
114
120
|
|
|
115
121
|
### Custom skills
|
|
@@ -144,6 +150,26 @@ relationships with `GraphQuery` — including **control inheritance** ("which
|
|
|
144
150
|
servers inherit physical controls from their enclave?"). Stdlib in-memory graph,
|
|
145
151
|
no Neo4j.
|
|
146
152
|
|
|
153
|
+
### STIG checklists (DISA `.ckl`/`.cklb`)
|
|
154
|
+
|
|
155
|
+
Take a raw DISA STIG benchmark all the way to a completed, eMASS/STIG-Viewer-
|
|
156
|
+
compatible checklist — entirely local (hostnames, IPs, and findings are CUI):
|
|
157
|
+
|
|
158
|
+
```
|
|
159
|
+
/stig new U_ASD_STIG_V6R1_Manual-xccdf.xml app.ckl # benchmark → blank .ckl
|
|
160
|
+
/graphrag build ./app # pull in the app's evidence
|
|
161
|
+
/loop 286 /stig-assess app.ckl # assess each rule vs evidence
|
|
162
|
+
/stig app.ckl open # list the open findings
|
|
163
|
+
/stig-remediate app.ckl SV-900010r1_rule # write an idempotent fix script
|
|
164
|
+
/stig graph app.ckl # ingest + auto-link rules → NIST controls
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
`/stig new` parses the XCCDF benchmark (validated against the full 286-rule
|
|
168
|
+
Application STIG); `/stig-assess` reads your evidence and writes status +
|
|
169
|
+
finding-details back in place; `/stig graph` builds `STIG`/`STIG-Rule` nodes and
|
|
170
|
+
**auto-links each rule to its NIST 800-53 control** through DISA's CCI map
|
|
171
|
+
(`Control —SATISFIED_BY→ rule`), fetched once and cached offline.
|
|
172
|
+
|
|
147
173
|
## Install
|
|
148
174
|
|
|
149
175
|
```bash
|
|
@@ -173,10 +199,11 @@ commands to do the work, showing each as a collapsible tool card.
|
|
|
173
199
|
`◡ Keelhauling… (12s · ↓ 6.2k tokens · thinking with high effort)`
|
|
174
200
|
- Submit while it's working and the prompt **queues** (drains in order)
|
|
175
201
|
- Slash commands: `/model` · `/cwd` · `/undo` (revert last write) · `/back`
|
|
176
|
-
(rewind last turn) · `/status` · `/compact` (shrink context) · `/
|
|
177
|
-
(
|
|
178
|
-
|
|
179
|
-
`/
|
|
202
|
+
(rewind last turn) · `/status` · `/compact` (shrink context) · `/context`
|
|
203
|
+
(view/set the context-window budget) · `/graphrag` (build/query a knowledge
|
|
204
|
+
base) · `/skills` (list your `/<name>` skills) · `/loop` (repeat a prompt) ·
|
|
205
|
+
`/mcp` (list MCP servers) · `/rmf` & `/stig` (NIST 800-53 / DISA STIG
|
|
206
|
+
automation) · `/clear` · `/help` · `/quit`
|
|
180
207
|
|
|
181
208
|
It honors `AGENTS.md` / `DRYDOCK.md` in the working directory for project
|
|
182
209
|
conventions.
|
|
@@ -67,10 +67,14 @@ Typed into the prompt. The agent also knows these, so you can just **ask it**
|
|
|
67
67
|
| `/mcp` | List connected MCP servers + their tools |
|
|
68
68
|
| `/rmf bootstrap [families]` | Ingest the NIST SP 800-53 catalog (RMF automation) |
|
|
69
69
|
| `/rmf-control` · `/rmf-categorize` · `/rmf-review` · `/rmf-poam` | Bundled RMF skills |
|
|
70
|
+
| `/stig new <xccdf>` | Generate a blank `.ckl` from a DISA STIG benchmark |
|
|
71
|
+
| `/stig <ckl>` · `/stig <ckl> open` | Summarize a checklist · list findings by status |
|
|
72
|
+
| `/stig graph <ckl>` | Ingest a checklist into the RMF graph (auto-links rules→controls via CCI) |
|
|
73
|
+
| `/stig-assess <ckl>` · `/stig-remediate <ckl> <rule>` | Assess a rule vs evidence · write a fix script |
|
|
70
74
|
| `/model` · `/cwd` | Show/set model & endpoint · working directory |
|
|
71
75
|
| `/undo` · `/back` | Revert the last write · rewind the last turn |
|
|
72
|
-
| `/compact` · `/
|
|
73
|
-
| `/help` · `/quit` |
|
|
76
|
+
| `/compact` · `/context [n]` | Shrink context now · view/set the context-window budget |
|
|
77
|
+
| `/status` · `/clear` · `/help` · `/quit` | Session stats · reset · help · exit |
|
|
74
78
|
|
|
75
79
|
### Knowledge base (GraphRAG) — ingesting your documents
|
|
76
80
|
|
|
@@ -83,9 +87,11 @@ Typed into the prompt. The agent also knows these, so you can just **ask it**
|
|
|
83
87
|
|
|
84
88
|
Once built, the agent **automatically** retrieves from it (read-only `Knowledge`
|
|
85
89
|
tool) when a question touches your material. Ingests text formats
|
|
86
|
-
(`.md .txt .py .js .json .yaml .sql …`) **
|
|
87
|
-
|
|
88
|
-
|
|
90
|
+
(`.md .txt .py .js .json .yaml .sql …`), **PDF and Word (`.docx`)**, and **STIG
|
|
91
|
+
checklists (`.ckl`/`.cklb`)** — checklists are flattened to per-rule findings so
|
|
92
|
+
you can ask "which findings are open?". `.docx`/`.ckl` need nothing extra; PDF
|
|
93
|
+
uses the `pdftotext` binary (poppler) if present, else `pip install
|
|
94
|
+
drydock-cli[pdf]` (pypdf). The index is a single JSON at
|
|
89
95
|
`<project>/.drydock/graphrag.json` — clean-room, no embeddings.
|
|
90
96
|
|
|
91
97
|
### Custom skills
|
|
@@ -120,6 +126,26 @@ relationships with `GraphQuery` — including **control inheritance** ("which
|
|
|
120
126
|
servers inherit physical controls from their enclave?"). Stdlib in-memory graph,
|
|
121
127
|
no Neo4j.
|
|
122
128
|
|
|
129
|
+
### STIG checklists (DISA `.ckl`/`.cklb`)
|
|
130
|
+
|
|
131
|
+
Take a raw DISA STIG benchmark all the way to a completed, eMASS/STIG-Viewer-
|
|
132
|
+
compatible checklist — entirely local (hostnames, IPs, and findings are CUI):
|
|
133
|
+
|
|
134
|
+
```
|
|
135
|
+
/stig new U_ASD_STIG_V6R1_Manual-xccdf.xml app.ckl # benchmark → blank .ckl
|
|
136
|
+
/graphrag build ./app # pull in the app's evidence
|
|
137
|
+
/loop 286 /stig-assess app.ckl # assess each rule vs evidence
|
|
138
|
+
/stig app.ckl open # list the open findings
|
|
139
|
+
/stig-remediate app.ckl SV-900010r1_rule # write an idempotent fix script
|
|
140
|
+
/stig graph app.ckl # ingest + auto-link rules → NIST controls
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
`/stig new` parses the XCCDF benchmark (validated against the full 286-rule
|
|
144
|
+
Application STIG); `/stig-assess` reads your evidence and writes status +
|
|
145
|
+
finding-details back in place; `/stig graph` builds `STIG`/`STIG-Rule` nodes and
|
|
146
|
+
**auto-links each rule to its NIST 800-53 control** through DISA's CCI map
|
|
147
|
+
(`Control —SATISFIED_BY→ rule`), fetched once and cached offline.
|
|
148
|
+
|
|
123
149
|
## Install
|
|
124
150
|
|
|
125
151
|
```bash
|
|
@@ -149,10 +175,11 @@ commands to do the work, showing each as a collapsible tool card.
|
|
|
149
175
|
`◡ Keelhauling… (12s · ↓ 6.2k tokens · thinking with high effort)`
|
|
150
176
|
- Submit while it's working and the prompt **queues** (drains in order)
|
|
151
177
|
- Slash commands: `/model` · `/cwd` · `/undo` (revert last write) · `/back`
|
|
152
|
-
(rewind last turn) · `/status` · `/compact` (shrink context) · `/
|
|
153
|
-
(
|
|
154
|
-
|
|
155
|
-
`/
|
|
178
|
+
(rewind last turn) · `/status` · `/compact` (shrink context) · `/context`
|
|
179
|
+
(view/set the context-window budget) · `/graphrag` (build/query a knowledge
|
|
180
|
+
base) · `/skills` (list your `/<name>` skills) · `/loop` (repeat a prompt) ·
|
|
181
|
+
`/mcp` (list MCP servers) · `/rmf` & `/stig` (NIST 800-53 / DISA STIG
|
|
182
|
+
automation) · `/clear` · `/help` · `/quit`
|
|
156
183
|
|
|
157
184
|
It honors `AGENTS.md` / `DRYDOCK.md` in the working directory for project
|
|
158
185
|
conventions.
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""CCI → NIST 800-53 control mapping.
|
|
2
|
+
|
|
3
|
+
DISA's Control Correlation Identifiers (CCIs) are the bridge between a STIG rule
|
|
4
|
+
and the NIST control it enforces: every STIG rule cites a CCI, and the CCI maps
|
|
5
|
+
to an 800-53 control. This builds that map from DISA's `U_CCI_List.xml` so
|
|
6
|
+
`/stig graph` can auto-create `Control —SATISFIED_BY→ STIG-Rule` edges.
|
|
7
|
+
|
|
8
|
+
The mappings are U.S. Government public-domain data (facts, not copyrightable).
|
|
9
|
+
The list is fetched once and cached locally; offline-safe. Stdlib only.
|
|
10
|
+
|
|
11
|
+
All logic original to Drydock.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import re
|
|
17
|
+
import urllib.error
|
|
18
|
+
import urllib.request
|
|
19
|
+
import xml.etree.ElementTree as ET
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
# DISA U_CCI_List.xml (official list, mirrored). Government public-domain data.
|
|
23
|
+
_CCI_URL = ("https://raw.githubusercontent.com/CyberSecDef/Cyber.Trackr.Live/master/"
|
|
24
|
+
"cyber.trackr.live/resources/data/cci/U_CCI_List.xml")
|
|
25
|
+
_UA = "drydock-cli"
|
|
26
|
+
_RE_CONTROL = re.compile(r"\s*([A-Za-z]{2})-(\d+)(?:\s*\(?(\d+)\)?|\.(\d+))?")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def cache_path(cwd: str) -> Path:
|
|
30
|
+
return Path(cwd) / ".drydock" / "rmf" / "cci_map.json"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _ln(el) -> str:
|
|
34
|
+
return el.tag.split("}")[-1]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _norm_control(index: str) -> str | None:
|
|
38
|
+
"""'AC-10' -> 'ac-10'; 'AC-2 (1)' / 'AC-2.1' -> 'ac-2.1'; junk -> None."""
|
|
39
|
+
m = _RE_CONTROL.match(index or "")
|
|
40
|
+
if not m:
|
|
41
|
+
return None
|
|
42
|
+
fam, num = m.group(1).lower(), m.group(2)
|
|
43
|
+
enh = m.group(3) or m.group(4)
|
|
44
|
+
return f"{fam}-{num}.{enh}" if enh else f"{fam}-{num}"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def parse_cci_list(path: str | Path) -> dict[str, str]:
|
|
48
|
+
"""Parse U_CCI_List.xml -> {CCI-id: control-id}. Picks the highest 800-53
|
|
49
|
+
revision reference (excludes 800-53A, which lists assessment procedures)."""
|
|
50
|
+
root = ET.parse(path).getroot()
|
|
51
|
+
out: dict[str, str] = {}
|
|
52
|
+
for item in (e for e in root.iter() if _ln(e) == "cci_item"):
|
|
53
|
+
cid = item.get("id")
|
|
54
|
+
best: str | None = None
|
|
55
|
+
best_ver = -1
|
|
56
|
+
for ref in item.iter():
|
|
57
|
+
if _ln(ref) != "reference":
|
|
58
|
+
continue
|
|
59
|
+
title = ref.get("title", "")
|
|
60
|
+
if not title.startswith("NIST SP 800-53") or "800-53A" in title:
|
|
61
|
+
continue
|
|
62
|
+
try:
|
|
63
|
+
ver = int(ref.get("version") or 0)
|
|
64
|
+
except ValueError:
|
|
65
|
+
ver = 0
|
|
66
|
+
ctl = _norm_control(ref.get("index", ""))
|
|
67
|
+
if ctl and ver >= best_ver:
|
|
68
|
+
best, best_ver = ctl, ver
|
|
69
|
+
if cid and best:
|
|
70
|
+
out[cid] = best
|
|
71
|
+
return out
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def fetch_to(path: str | Path, *, timeout: float = 60.0) -> None:
|
|
75
|
+
req = urllib.request.Request(_CCI_URL, headers={"User-Agent": _UA})
|
|
76
|
+
with urllib.request.urlopen(req, timeout=timeout) as r: # noqa: S310 (trusted host)
|
|
77
|
+
data = r.read()
|
|
78
|
+
Path(path).write_bytes(data)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def load_map(cwd: str, *, refresh: bool = False) -> dict[str, str]:
|
|
82
|
+
"""Return the cached {CCI: control} map, building it from U_CCI_List.xml on
|
|
83
|
+
first use (or --refresh). Offline-safe: returns the cache if the fetch fails,
|
|
84
|
+
or {} if there's no cache and no network."""
|
|
85
|
+
cp = cache_path(cwd)
|
|
86
|
+
if cp.exists() and not refresh:
|
|
87
|
+
try:
|
|
88
|
+
return json.loads(cp.read_text("utf-8"))
|
|
89
|
+
except (OSError, ValueError):
|
|
90
|
+
pass
|
|
91
|
+
cp.parent.mkdir(parents=True, exist_ok=True) # ensure .drydock/rmf/ exists before writing
|
|
92
|
+
raw = cp.with_suffix(".xml")
|
|
93
|
+
try:
|
|
94
|
+
fetch_to(raw)
|
|
95
|
+
except (urllib.error.URLError, OSError):
|
|
96
|
+
if cp.exists():
|
|
97
|
+
try:
|
|
98
|
+
return json.loads(cp.read_text("utf-8"))
|
|
99
|
+
except (OSError, ValueError):
|
|
100
|
+
return {}
|
|
101
|
+
return {}
|
|
102
|
+
mapping = parse_cci_list(raw)
|
|
103
|
+
cp.parent.mkdir(parents=True, exist_ok=True)
|
|
104
|
+
cp.write_text(json.dumps(mapping), "utf-8")
|
|
105
|
+
try:
|
|
106
|
+
raw.unlink()
|
|
107
|
+
except OSError:
|
|
108
|
+
pass
|
|
109
|
+
return mapping
|
|
@@ -126,11 +126,11 @@ def rule_node(rid: str) -> str:
|
|
|
126
126
|
return f"stigrule:{rid.strip().lower()}"
|
|
127
127
|
|
|
128
128
|
|
|
129
|
-
def ingest_checklist(g: "RmfGraph", checklist) -> dict:
|
|
129
|
+
def ingest_checklist(g: "RmfGraph", checklist, cci_map: dict | None = None) -> dict:
|
|
130
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).
|
|
132
|
-
|
|
133
|
-
|
|
131
|
+
PART_OF (rule→stig), APPLIES_TO (stig→host), EVALUATES (rule→host). When a
|
|
132
|
+
CCI→control map is supplied, also auto-creates Control —SATISFIED_BY→ STIG-Rule
|
|
133
|
+
edges via each rule's CCI. Returns {rules, host, linked}."""
|
|
134
134
|
host = (checklist.asset.get("HOST_NAME") or checklist.asset.get("host_name") or "target")
|
|
135
135
|
hnode = component_id(host)
|
|
136
136
|
g.add_node(hnode, "Component", name=host, ip=checklist.asset.get("HOST_IP"))
|
|
@@ -138,6 +138,7 @@ def ingest_checklist(g: "RmfGraph", checklist) -> dict:
|
|
|
138
138
|
snode = stig_id(sname)
|
|
139
139
|
g.add_node(snode, "STIG", name=sname, version=checklist.stig_version)
|
|
140
140
|
g.add_edge(snode, "APPLIES_TO", hnode)
|
|
141
|
+
linked = 0
|
|
141
142
|
for r in checklist.rules:
|
|
142
143
|
rid = r.rule_id or r.group_id
|
|
143
144
|
if not rid:
|
|
@@ -147,7 +148,14 @@ def ingest_checklist(g: "RmfGraph", checklist) -> dict:
|
|
|
147
148
|
severity=r.severity, status=r.status, cci=r.cci, title=r.title)
|
|
148
149
|
g.add_edge(rn, "PART_OF", snode)
|
|
149
150
|
g.add_edge(rn, "EVALUATES", hnode)
|
|
150
|
-
|
|
151
|
+
ctl = (cci_map or {}).get(r.cci)
|
|
152
|
+
if ctl:
|
|
153
|
+
cn = control_id(ctl)
|
|
154
|
+
if not g.get(cn):
|
|
155
|
+
g.add_node(cn, "Control", control_id=ctl.upper())
|
|
156
|
+
g.add_edge(cn, "SATISFIED_BY", rn)
|
|
157
|
+
linked += 1
|
|
158
|
+
return {"rules": len(checklist.rules), "host": host, "linked": linked}
|
|
151
159
|
|
|
152
160
|
|
|
153
161
|
def build_from_catalog(catalog: dict, *, families: list[str] | None = None) -> RmfGraph:
|
|
@@ -1017,15 +1017,34 @@ _SUBAGENT_SYSTEM = (
|
|
|
1017
1017
|
"tools: Read, Glob, Grep, and Bash (use Bash only to INSPECT — ls, cat, "
|
|
1018
1018
|
"grep, find, git log — never to modify files). Investigate the task you are "
|
|
1019
1019
|
"given, then STOP and reply with a concise, factual summary of what you "
|
|
1020
|
-
"found: concrete file:line references and the key code
|
|
1021
|
-
"
|
|
1020
|
+
"found: concrete file:line references and only the few key code snippets that "
|
|
1021
|
+
"matter, not narration and not whole files. Aim for under ~250 words — the "
|
|
1022
|
+
"main agent only gets this summary (not your tool output), so distill, don't "
|
|
1023
|
+
"dump. Do NOT try to edit or create files — the main agent acts on your findings."
|
|
1022
1024
|
)
|
|
1023
1025
|
|
|
1024
1026
|
|
|
1027
|
+
# A sub-agent's whole job is to keep its investigation OUT of the main agent's
|
|
1028
|
+
# context and hand back only a partition. The system prompt asks for a concise
|
|
1029
|
+
# summary, but a runaway model could still return a wall of text — so cap what
|
|
1030
|
+
# crosses back into the parent's window. ~4000 chars ≈ ~1000 tokens.
|
|
1031
|
+
_SUBAGENT_SUMMARY_CAP = 4000
|
|
1032
|
+
|
|
1033
|
+
|
|
1034
|
+
def _cap_summary(text: str) -> str:
|
|
1035
|
+
if len(text) <= _SUBAGENT_SUMMARY_CAP:
|
|
1036
|
+
return text
|
|
1037
|
+
head = text[:_SUBAGENT_SUMMARY_CAP].rsplit("\n", 1)[0] or text[:_SUBAGENT_SUMMARY_CAP]
|
|
1038
|
+
dropped = len(text) - len(head)
|
|
1039
|
+
return (head + f"\n[… sub-agent summary truncated, {dropped} chars dropped to keep it out "
|
|
1040
|
+
"of the main context. Ask a narrower follow-up sub-agent task if you need more.]")
|
|
1041
|
+
|
|
1042
|
+
|
|
1025
1043
|
def _run_subagent(prompt: str, config: dict) -> str:
|
|
1026
1044
|
"""Run one read-only sub-agent to completion and return its final summary.
|
|
1027
1045
|
Shared by `task` (one) and `Dispatch` (many in parallel). Hard-capped; never
|
|
1028
|
-
raises (a sub-agent must not crash the parent turn).
|
|
1046
|
+
raises (a sub-agent must not crash the parent turn). The returned summary is
|
|
1047
|
+
size-capped (_cap_summary) so a sub-agent can never bloat the main context."""
|
|
1029
1048
|
from drydock.agent import run as agent_run, AgentState, TurnDone
|
|
1030
1049
|
|
|
1031
1050
|
sub_state = AgentState()
|
|
@@ -1048,7 +1067,7 @@ def _run_subagent(prompt: str, config: dict) -> str:
|
|
|
1048
1067
|
return f"[sub-agent error: {e}]"
|
|
1049
1068
|
for msg in reversed(sub_state.messages):
|
|
1050
1069
|
if msg.get("role") == "assistant" and (msg.get("content") or "").strip():
|
|
1051
|
-
return msg["content"].strip()
|
|
1070
|
+
return _cap_summary(msg["content"].strip())
|
|
1052
1071
|
return f"[sub-agent finished {steps} step(s) with no summary]"
|
|
1053
1072
|
|
|
1054
1073
|
|
|
@@ -563,21 +563,10 @@ class DrydockApp(App):
|
|
|
563
563
|
return
|
|
564
564
|
# /stig graph <path> — ingest the checklist into the RMF typed graph
|
|
565
565
|
if parts[0].lower() == "graph" and len(parts) > 1:
|
|
566
|
-
from drydock import rmf_graph
|
|
567
566
|
gp = parts[1] if _os.path.isabs(parts[1]) else _os.path.join(cwd, parts[1])
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
r = rmf_graph.ingest_checklist(g, cl)
|
|
572
|
-
g.save(rmf_graph.graph_path(cwd))
|
|
573
|
-
except Exception as e: # noqa: BLE001
|
|
574
|
-
self._mount(ErrorMessage(f"could not graph checklist: {e}"))
|
|
575
|
-
return
|
|
576
|
-
self._info(
|
|
577
|
-
f"✓ Ingested {r['rules']} STIG rules for host '{r['host']}' into the "
|
|
578
|
-
f"RMF graph (STIG/STIG-Rule nodes, PART_OF/APPLIES_TO/EVALUATES). "
|
|
579
|
-
"Link rules to controls with GraphAdd satisfies; trace via GraphQuery."
|
|
580
|
-
)
|
|
567
|
+
self._info("Ingesting the checklist into the RMF graph "
|
|
568
|
+
"(fetching the DISA CCI→800-53 map on first use)…")
|
|
569
|
+
self.run_worker(lambda: self._stig_graph(cwd, gp), thread=True)
|
|
581
570
|
return
|
|
582
571
|
path = parts[0]
|
|
583
572
|
if not _os.path.isabs(path):
|
|
@@ -643,6 +632,30 @@ class DrydockApp(App):
|
|
|
643
632
|
"catalog download; after that it works offline.)")
|
|
644
633
|
self.call_from_thread(self._info, msg)
|
|
645
634
|
|
|
635
|
+
def _stig_graph(self, cwd: str, path: str) -> None:
|
|
636
|
+
"""Worker-thread body: ingest a checklist into the RMF graph, auto-linking
|
|
637
|
+
rules to NIST controls via the DISA CCI map. Reports back on the UI."""
|
|
638
|
+
from drydock import cci, rmf_graph, stig
|
|
639
|
+
|
|
640
|
+
try:
|
|
641
|
+
cl = stig.load(path)
|
|
642
|
+
cci_map = cci.load_map(cwd) # fetch+cache once; offline-safe ({} on failure)
|
|
643
|
+
g = rmf_graph.RmfGraph.load(rmf_graph.graph_path(cwd))
|
|
644
|
+
r = rmf_graph.ingest_checklist(g, cl, cci_map)
|
|
645
|
+
g.save(rmf_graph.graph_path(cwd))
|
|
646
|
+
link_note = (
|
|
647
|
+
f"auto-linked {r['linked']}/{r['rules']} rules to NIST controls via CCI "
|
|
648
|
+
"(Control —SATISFIED_BY→ rule). Trace with GraphQuery control <id>."
|
|
649
|
+
if r["linked"] else
|
|
650
|
+
"(no CCI→control links — the CCI map was unavailable offline; rules are "
|
|
651
|
+
"still in the graph. Re-run online, or use GraphAdd satisfies.)"
|
|
652
|
+
)
|
|
653
|
+
msg = (f"✓ Ingested {r['rules']} STIG rules for host '{r['host']}' into the "
|
|
654
|
+
f"RMF graph (STIG/STIG-Rule + PART_OF/APPLIES_TO/EVALUATES); {link_note}")
|
|
655
|
+
except Exception as e: # noqa: BLE001
|
|
656
|
+
msg = f"could not graph checklist: {e}"
|
|
657
|
+
self.call_from_thread(self._info, msg)
|
|
658
|
+
|
|
646
659
|
def _cmd_mcp(self) -> None:
|
|
647
660
|
"""List connected MCP servers and the tools they expose."""
|
|
648
661
|
from drydock import mcp
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: drydock-cli
|
|
3
|
-
Version: 3.0.
|
|
3
|
+
Version: 3.0.75
|
|
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
|
|
@@ -91,10 +91,14 @@ Typed into the prompt. The agent also knows these, so you can just **ask it**
|
|
|
91
91
|
| `/mcp` | List connected MCP servers + their tools |
|
|
92
92
|
| `/rmf bootstrap [families]` | Ingest the NIST SP 800-53 catalog (RMF automation) |
|
|
93
93
|
| `/rmf-control` · `/rmf-categorize` · `/rmf-review` · `/rmf-poam` | Bundled RMF skills |
|
|
94
|
+
| `/stig new <xccdf>` | Generate a blank `.ckl` from a DISA STIG benchmark |
|
|
95
|
+
| `/stig <ckl>` · `/stig <ckl> open` | Summarize a checklist · list findings by status |
|
|
96
|
+
| `/stig graph <ckl>` | Ingest a checklist into the RMF graph (auto-links rules→controls via CCI) |
|
|
97
|
+
| `/stig-assess <ckl>` · `/stig-remediate <ckl> <rule>` | Assess a rule vs evidence · write a fix script |
|
|
94
98
|
| `/model` · `/cwd` | Show/set model & endpoint · working directory |
|
|
95
99
|
| `/undo` · `/back` | Revert the last write · rewind the last turn |
|
|
96
|
-
| `/compact` · `/
|
|
97
|
-
| `/help` · `/quit` |
|
|
100
|
+
| `/compact` · `/context [n]` | Shrink context now · view/set the context-window budget |
|
|
101
|
+
| `/status` · `/clear` · `/help` · `/quit` | Session stats · reset · help · exit |
|
|
98
102
|
|
|
99
103
|
### Knowledge base (GraphRAG) — ingesting your documents
|
|
100
104
|
|
|
@@ -107,9 +111,11 @@ Typed into the prompt. The agent also knows these, so you can just **ask it**
|
|
|
107
111
|
|
|
108
112
|
Once built, the agent **automatically** retrieves from it (read-only `Knowledge`
|
|
109
113
|
tool) when a question touches your material. Ingests text formats
|
|
110
|
-
(`.md .txt .py .js .json .yaml .sql …`) **
|
|
111
|
-
|
|
112
|
-
|
|
114
|
+
(`.md .txt .py .js .json .yaml .sql …`), **PDF and Word (`.docx`)**, and **STIG
|
|
115
|
+
checklists (`.ckl`/`.cklb`)** — checklists are flattened to per-rule findings so
|
|
116
|
+
you can ask "which findings are open?". `.docx`/`.ckl` need nothing extra; PDF
|
|
117
|
+
uses the `pdftotext` binary (poppler) if present, else `pip install
|
|
118
|
+
drydock-cli[pdf]` (pypdf). The index is a single JSON at
|
|
113
119
|
`<project>/.drydock/graphrag.json` — clean-room, no embeddings.
|
|
114
120
|
|
|
115
121
|
### Custom skills
|
|
@@ -144,6 +150,26 @@ relationships with `GraphQuery` — including **control inheritance** ("which
|
|
|
144
150
|
servers inherit physical controls from their enclave?"). Stdlib in-memory graph,
|
|
145
151
|
no Neo4j.
|
|
146
152
|
|
|
153
|
+
### STIG checklists (DISA `.ckl`/`.cklb`)
|
|
154
|
+
|
|
155
|
+
Take a raw DISA STIG benchmark all the way to a completed, eMASS/STIG-Viewer-
|
|
156
|
+
compatible checklist — entirely local (hostnames, IPs, and findings are CUI):
|
|
157
|
+
|
|
158
|
+
```
|
|
159
|
+
/stig new U_ASD_STIG_V6R1_Manual-xccdf.xml app.ckl # benchmark → blank .ckl
|
|
160
|
+
/graphrag build ./app # pull in the app's evidence
|
|
161
|
+
/loop 286 /stig-assess app.ckl # assess each rule vs evidence
|
|
162
|
+
/stig app.ckl open # list the open findings
|
|
163
|
+
/stig-remediate app.ckl SV-900010r1_rule # write an idempotent fix script
|
|
164
|
+
/stig graph app.ckl # ingest + auto-link rules → NIST controls
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
`/stig new` parses the XCCDF benchmark (validated against the full 286-rule
|
|
168
|
+
Application STIG); `/stig-assess` reads your evidence and writes status +
|
|
169
|
+
finding-details back in place; `/stig graph` builds `STIG`/`STIG-Rule` nodes and
|
|
170
|
+
**auto-links each rule to its NIST 800-53 control** through DISA's CCI map
|
|
171
|
+
(`Control —SATISFIED_BY→ rule`), fetched once and cached offline.
|
|
172
|
+
|
|
147
173
|
## Install
|
|
148
174
|
|
|
149
175
|
```bash
|
|
@@ -173,10 +199,11 @@ commands to do the work, showing each as a collapsible tool card.
|
|
|
173
199
|
`◡ Keelhauling… (12s · ↓ 6.2k tokens · thinking with high effort)`
|
|
174
200
|
- Submit while it's working and the prompt **queues** (drains in order)
|
|
175
201
|
- Slash commands: `/model` · `/cwd` · `/undo` (revert last write) · `/back`
|
|
176
|
-
(rewind last turn) · `/status` · `/compact` (shrink context) · `/
|
|
177
|
-
(
|
|
178
|
-
|
|
179
|
-
`/
|
|
202
|
+
(rewind last turn) · `/status` · `/compact` (shrink context) · `/context`
|
|
203
|
+
(view/set the context-window budget) · `/graphrag` (build/query a knowledge
|
|
204
|
+
base) · `/skills` (list your `/<name>` skills) · `/loop` (repeat a prompt) ·
|
|
205
|
+
`/mcp` (list MCP servers) · `/rmf` & `/stig` (NIST 800-53 / DISA STIG
|
|
206
|
+
automation) · `/clear` · `/help` · `/quit`
|
|
180
207
|
|
|
181
208
|
It honors `AGENTS.md` / `DRYDOCK.md` in the working directory for project
|
|
182
209
|
conventions.
|
|
@@ -6,6 +6,7 @@ drydock/__init__.py
|
|
|
6
6
|
drydock/__main__.py
|
|
7
7
|
drydock/agent.py
|
|
8
8
|
drydock/bash_safety.py
|
|
9
|
+
drydock/cci.py
|
|
9
10
|
drydock/cli.py
|
|
10
11
|
drydock/compaction.py
|
|
11
12
|
drydock/config.py
|
|
@@ -49,6 +50,7 @@ tests/test_bash_output_bounding.py
|
|
|
49
50
|
tests/test_bash_process_group.py
|
|
50
51
|
tests/test_bash_safety.py
|
|
51
52
|
tests/test_bash_timeout_network.py
|
|
53
|
+
tests/test_cci.py
|
|
52
54
|
tests/test_cli_agents.py
|
|
53
55
|
tests/test_compact_command.py
|
|
54
56
|
tests/test_compaction.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.75"
|
|
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,82 @@
|
|
|
1
|
+
"""CCI → NIST 800-53 control map (the STIG-rule ↔ control bridge)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from drydock import cci, rmf_graph, stig
|
|
5
|
+
|
|
6
|
+
_CCI_XML = '''<?xml version="1.0"?><cci_list><cci_items>
|
|
7
|
+
<cci_item id="CCI-000366"><references>
|
|
8
|
+
<reference title="NIST SP 800-53" version="3" index="CM-6"/>
|
|
9
|
+
<reference title="NIST SP 800-53A" version="1" index="CM-6.1 (ii)"/>
|
|
10
|
+
<reference title="NIST SP 800-53 Revision 4" version="4" index="CM-6 b"/></references></cci_item>
|
|
11
|
+
<cci_item id="CCI-000054"><references>
|
|
12
|
+
<reference title="NIST SP 800-53 Revision 5" version="5" index="AC-10"/></references></cci_item>
|
|
13
|
+
<cci_item id="CCI-001133"><references>
|
|
14
|
+
<reference title="NIST SP 800-53 Revision 4" version="4" index="SC-10 (1)"/></references></cci_item>
|
|
15
|
+
<cci_item id="CCI-999999"><references>
|
|
16
|
+
<reference title="NIST SP 800-53A" version="1" index="ZZ-9.9"/></references></cci_item>
|
|
17
|
+
</cci_items></cci_list>'''
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_parse_picks_highest_rev_excludes_53A(tmp_path):
|
|
21
|
+
p = tmp_path / "cci.xml"; p.write_text(_CCI_XML)
|
|
22
|
+
m = cci.parse_cci_list(p)
|
|
23
|
+
assert m["CCI-000366"] == "cm-6" # rev4 base, 800-53A excluded
|
|
24
|
+
assert m["CCI-000054"] == "ac-10"
|
|
25
|
+
assert m["CCI-001133"] == "sc-10.1" # enhancement normalized
|
|
26
|
+
assert "CCI-999999" not in m # only 800-53A ref → no control
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_norm_control_formats():
|
|
30
|
+
assert cci._norm_control("AC-10") == "ac-10"
|
|
31
|
+
assert cci._norm_control("AC-2 (1)") == "ac-2.1"
|
|
32
|
+
assert cci._norm_control("AC-2.1") == "ac-2.1"
|
|
33
|
+
assert cci._norm_control("garbage") is None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_load_map_caches_and_is_offline_safe(tmp_path, monkeypatch):
|
|
37
|
+
# no network: with no cache, returns {} (never raises)
|
|
38
|
+
def boom(*a, **k):
|
|
39
|
+
raise OSError("offline")
|
|
40
|
+
monkeypatch.setattr(cci, "fetch_to", boom)
|
|
41
|
+
assert cci.load_map(str(tmp_path)) == {}
|
|
42
|
+
# seed a cache → load_map returns it without fetching
|
|
43
|
+
cp = cci.cache_path(str(tmp_path)); cp.parent.mkdir(parents=True, exist_ok=True)
|
|
44
|
+
cp.write_text('{"CCI-000366": "cm-6"}')
|
|
45
|
+
assert cci.load_map(str(tmp_path)) == {"CCI-000366": "cm-6"}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_ingest_auto_links_via_cci(tmp_path):
|
|
49
|
+
(tmp_path / "s.ckl").write_text(
|
|
50
|
+
'<?xml version="1.0"?><CHECKLIST><ASSET><HOST_NAME>h1</HOST_NAME></ASSET><STIGS><iSTIG>'
|
|
51
|
+
'<STIG_INFO></STIG_INFO><VULN>'
|
|
52
|
+
'<STIG_DATA><VULN_ATTRIBUTE>Rule_ID</VULN_ATTRIBUTE><ATTRIBUTE_DATA>SV-9</ATTRIBUTE_DATA></STIG_DATA>'
|
|
53
|
+
'<STIG_DATA><VULN_ATTRIBUTE>CCI_REF</VULN_ATTRIBUTE><ATTRIBUTE_DATA>CCI-000366</ATTRIBUTE_DATA></STIG_DATA>'
|
|
54
|
+
'<STATUS>Open</STATUS></VULN></iSTIG></STIGS></CHECKLIST>')
|
|
55
|
+
cl = stig.load(tmp_path / "s.ckl")
|
|
56
|
+
g = rmf_graph.RmfGraph()
|
|
57
|
+
r = rmf_graph.ingest_checklist(g, cl, {"CCI-000366": "cm-6"})
|
|
58
|
+
assert r["linked"] == 1
|
|
59
|
+
assert rmf_graph.rule_node("SV-9") in g.neighbors(rmf_graph.control_id("cm-6"), "SATISFIED_BY")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_ingest_without_map_does_not_link(tmp_path):
|
|
63
|
+
(tmp_path / "s.ckl").write_text(
|
|
64
|
+
'<?xml version="1.0"?><CHECKLIST><STIGS><iSTIG><STIG_INFO></STIG_INFO><VULN>'
|
|
65
|
+
'<STIG_DATA><VULN_ATTRIBUTE>Rule_ID</VULN_ATTRIBUTE><ATTRIBUTE_DATA>SV-9</ATTRIBUTE_DATA></STIG_DATA>'
|
|
66
|
+
'<STATUS>Open</STATUS></VULN></iSTIG></STIGS></CHECKLIST>')
|
|
67
|
+
cl = stig.load(tmp_path / "s.ckl")
|
|
68
|
+
r = rmf_graph.ingest_checklist(rmf_graph.RmfGraph(), cl)
|
|
69
|
+
assert r["linked"] == 0
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_load_map_creates_dir_and_caches(tmp_path, monkeypatch):
|
|
73
|
+
"""Regression: the fetch must create .drydock/rmf/ before writing — a real
|
|
74
|
+
fetch failed because the dir didn't exist yet (found via hands-on TUI test)."""
|
|
75
|
+
def fake_fetch(path, **kw):
|
|
76
|
+
# a real fetch writes to .drydock/rmf/cci_map.xml — the dir must exist
|
|
77
|
+
from pathlib import Path
|
|
78
|
+
Path(path).write_text(_CCI_XML)
|
|
79
|
+
monkeypatch.setattr(cci, "fetch_to", fake_fetch)
|
|
80
|
+
m = cci.load_map(str(tmp_path))
|
|
81
|
+
assert m["CCI-000366"] == "cm-6" # parsed from the "fetched" list
|
|
82
|
+
assert cci.cache_path(str(tmp_path)).exists() # cached as json
|
|
@@ -60,3 +60,14 @@ def test_task_still_works_via_shared_runner(monkeypatch):
|
|
|
60
60
|
monkeypatch.setattr(T, "_run_subagent", lambda p, c: f"summary:{p}")
|
|
61
61
|
assert T.tool_task({"prompt": "explore X"}, {}) == "summary:explore X"
|
|
62
62
|
assert "needs a `prompt`" in T.tool_task({}, {})
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_subagent_summary_is_capped():
|
|
66
|
+
"""A sub-agent's return is size-capped so its investigation can never bloat
|
|
67
|
+
the main agent's context — only a bounded partition crosses back."""
|
|
68
|
+
from drydock.tools import _cap_summary, _SUBAGENT_SUMMARY_CAP
|
|
69
|
+
assert _cap_summary("brief finding") == "brief finding" # short passes through
|
|
70
|
+
big = "\n".join(f"line {i} with some detail" for i in range(2000))
|
|
71
|
+
out = _cap_summary(big)
|
|
72
|
+
assert len(out) < _SUBAGENT_SUMMARY_CAP + 200 # bounded
|
|
73
|
+
assert "truncated" in out and "main context" in out # explains the cut
|
|
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
|