@stroq/cli 0.5.1 → 0.11.0
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.
- package/README.md +32 -12
- package/dist/asi.json +19 -0
- package/dist/atlas.json +993 -0
- package/dist/corpus.json +777 -0
- package/dist/index.js +4306 -962
- package/dist/scope.json +426 -0
- package/openclaw-plugin/README.md +49 -0
- package/openclaw-plugin/index.js +146 -0
- package/openclaw-plugin/openclaw.plugin.json +27 -0
- package/openclaw-plugin/package.json +17 -0
- package/openclaw-plugin/run-stroq.js +166 -0
- package/package.json +8 -7
- package/dist/index.js.map +0 -1
package/dist/scope.json
ADDED
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
{
|
|
2
|
+
"atlasRelease": "2026.08",
|
|
3
|
+
"inScope": [
|
|
4
|
+
{
|
|
5
|
+
"id": "AML.T0010",
|
|
6
|
+
"limitation": "Stroq sees the agent’s own install or load of a supply-chain artifact (a package, model, container image, or tool); compromise introduced before the agent ever touches the artifact (e.g. at the origin registry) is addressed under supply-chain staging, not here."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"id": "AML.T0010.001",
|
|
10
|
+
"limitation": "Covers the package-install and load commands the agent runs; a compromised dependency pulled in transitively by another tool without the agent’s own visible install step is not separately detected."
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"id": "AML.T0010.003",
|
|
14
|
+
"limitation": "Covers the agent’s own download and load of a model file; whether the loaded weights are themselves poisoned is a model-attack-surface question this file cannot answer, only that the load and any embedded executable payload are visible."
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"id": "AML.T0010.004",
|
|
18
|
+
"limitation": "Covers a `docker pull` or equivalent the agent runs; an image pulled by a CI/CD pipeline outside the agent’s own tool calls is invisible."
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"id": "AML.T0010.005",
|
|
22
|
+
"limitation": "Covers installing or connecting a poisoned tool through the agent’s own tool calls; a tool added directly through an IDE or platform UI outside those calls is not seen."
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"id": "AML.T0011",
|
|
26
|
+
"limitation": "Covers execution the agent itself triggers via a tool call; execution a human user triggers directly, outside the agent, is not this product’s surface."
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"id": "AML.T0011.000",
|
|
30
|
+
"limitation": "Covers the agent loading or executing the artifact via a tool call; a runtime that deserializes the artifact internally, with no distinct tool call for the load, is invisible."
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"id": "AML.T0011.001",
|
|
34
|
+
"limitation": "Covers the install and any immediate execution the agent’s tool calls perform; a package that behaves maliciously only long after install, with no further agent-visible action, is not caught at install time."
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"id": "AML.T0011.002",
|
|
38
|
+
"limitation": "Covers invocation of the poisoned tool through the agent’s own tool-call channel; a tool that behaves normally when called but corrupts state elsewhere with no reflected content is not distinguishable from a benign one."
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"id": "AML.T0050",
|
|
42
|
+
"limitation": "Covers commands submitted through the agent’s own shell or interpreter tool call; a command an interpreter runs internally, without a distinct tool-call boundary, is invisible."
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"id": "AML.T0051",
|
|
46
|
+
"limitation": "Covers injected instructions in content the agent reads back through a tool call; instructions injected purely in a user’s own conversational turn, never touching a tool, are not this surface."
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"id": "AML.T0051.000",
|
|
50
|
+
"limitation": "Direct injection happens in the user’s own prompt to the model, a channel Stroq’s hooks do not see; Stroq can only act once the injected instruction drives a subsequent tool call."
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"id": "AML.T0051.001",
|
|
54
|
+
"limitation": null
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"id": "AML.T0051.002",
|
|
58
|
+
"limitation": "Covers the triggering event when it arrives as tool-call content (a file change, an incoming message ingested via a tool); an event source entirely outside any tool call is invisible."
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"id": "AML.T0068",
|
|
62
|
+
"limitation": "Stroq’s normalization pipeline targets the known obfuscation classes (hidden text, encoding, confusables); its own mutation fuzzer has already demonstrated encodings that evade it, so this is a claim of intent to detect, not of catching every encoding."
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"id": "AML.T0078",
|
|
66
|
+
"limitation": "Covers the case where the malicious page is fetched by the agent as ordinary web content and its prompt injection is evaluated like any other; a browser exploit unrelated to AI content, or a page visited by the human directly, is outside this product’s surface."
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"id": "AML.T0093",
|
|
70
|
+
"limitation": "Covers the moment the planted content is later read back by the agent through a tool call; the initial planting on the public-facing application itself is not something a developer’s local hook observes."
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"id": "AML.T0094",
|
|
74
|
+
"limitation": "Stroq evaluates each tool call independently and does not track that a delayed instruction seen earlier in a session is about to fire; a rule that would catch the instruction immediately can still miss it once its execution is deferred past the point of first exposure."
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"id": "AML.T0123",
|
|
78
|
+
"limitation": "Same limitation as prompt obfuscation: Stroq’s normalization and decode-and-rescan logic targets known encodings, and its own fuzzer has shown specific encodings that currently evade it."
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"id": "AML.T0053",
|
|
82
|
+
"limitation": null
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"id": "AML.T0086",
|
|
86
|
+
"limitation": null
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"id": "AML.T0101",
|
|
90
|
+
"limitation": null
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"id": "AML.T0108",
|
|
94
|
+
"limitation": "Stroq evaluates each tool call the compromised agent makes on its own merits (a fetch, then a destructive or exfiltrating action); it does not itself recognize a fetch-and-execute loop as a C2 beacon pattern across a session."
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
"id": "AML.T0080",
|
|
98
|
+
"limitation": "Covers poisoning delivered through content the agent reads via a tool call; poisoning injected purely through direct conversation with no tool-call boundary is not this surface."
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"id": "AML.T0080.000",
|
|
102
|
+
"limitation": "Covers a memory write that flows through a tool call Stroq can inspect; a memory feature implemented as an opaque internal model state with no corresponding tool call is invisible."
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
"id": "AML.T0080.001",
|
|
106
|
+
"limitation": "Covers thread content the agent reads back through a tool call; instructions injected purely as user conversation turns are not visible to a hook that only sees tool calls."
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
"id": "AML.T0081",
|
|
110
|
+
"limitation": "Covers the one configuration surface the corpus currently exercises — the `hooks` block of `.claude/settings.json`, overwritten via a `Write` tool call; the technique's own description also spans the system prompt, knowledge sources, and other connected-tool settings, none of which any scenario in the suite currently drives through this technique."
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
"id": "AML.T0083",
|
|
114
|
+
"limitation": "Covers the agent’s own read of its configuration file through a tool call; credentials extracted by a process outside the agent’s tool-call surface (e.g. a separate script reading the same file) are invisible."
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
"id": "AML.T0084",
|
|
118
|
+
"limitation": "Covers configuration discovered by reading a file through a tool call; configuration details volunteered by the model when a user simply asks it questions, with no tool call involved, are not visible."
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
"id": "AML.T0084.000",
|
|
122
|
+
"limitation": "Same limitation as the parent: visible only when discovery happens via a file or tool-call read, not via conversational questioning of the model."
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
"id": "AML.T0084.001",
|
|
126
|
+
"limitation": "Visible when tool definitions are read from a configuration file via a tool call; definitions the model recites from its own loaded context, with no corresponding file read, are not visible."
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
"id": "AML.T0084.002",
|
|
130
|
+
"limitation": "Visible when trigger configuration is read via a tool call; triggers inferred purely by observing agent behavior over time are not something a per-call hook reconstructs."
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
"id": "AML.T0084.003",
|
|
134
|
+
"limitation": "Visible when call-chain information is read from configuration or source files via a tool call; chains inferred by static analysis of code never read through the agent’s own tools are not covered."
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
"id": "AML.T0070",
|
|
138
|
+
"limitation": "Covers the poisoned content once the agent’s tool retrieves and surfaces it for the model to read; the initial placement of that content into the RAG store is a separate action outside the developer’s machine."
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
"id": "AML.T0071",
|
|
142
|
+
"limitation": "Same limitation as RAG Poisoning: Stroq sees the retrieved content when a tool surfaces it, not the injection into the RAG store itself."
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
"id": "AML.T0082",
|
|
146
|
+
"limitation": "Covers the retrieval tool call and the credential-shaped content it returns; credentials mixed into a huge, unremarkable-looking retrieval result may not be distinguishable from ordinary document content by a content rule alone."
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
"id": "AML.T0085",
|
|
150
|
+
"limitation": "Covers data movement visible through the agent’s own tool calls to connected services; data the AI service itself discloses through a channel outside any tool call is not visible."
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
"id": "AML.T0085.000",
|
|
154
|
+
"limitation": "Same limitation as the parent: visible through the retrieval tool call and its returned content, not through any other access path to the RAG store."
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
"id": "AML.T0085.001",
|
|
158
|
+
"limitation": "Same limitation as the parent: visible through the tool invocation itself, not through any access path outside the agent’s own tool calls."
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
"id": "AML.T0098",
|
|
162
|
+
"limitation": "Covers credential-shaped content returned by a tool call Stroq inspects; credentials embedded in a format or encoding a content rule does not recognize may pass unflagged."
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
"id": "AML.T0099",
|
|
166
|
+
"limitation": "Covers the poisoned content once an agent tool retrieves it; the poisoning of the underlying data source itself happens outside the developer’s machine and is not directly observed."
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
"id": "AML.T0110",
|
|
170
|
+
"limitation": "Covers a poisoned tool’s effects once it is invoked or its response is read through a tool call; a tool’s static, model-visible description that is never reflected back through a hook-observed call may not be inspected at all."
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
"id": "AML.T0110.000",
|
|
174
|
+
"limitation": "Stroq observes a tool’s invocation and its response, not necessarily the static tool definition or schema loaded into the model’s context at session start; a poisoned description that never causes an anomalous call or response can be invisible."
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
"id": "AML.T0110.001",
|
|
178
|
+
"limitation": "Covers the tool’s actual invocation and response as Stroq sees them; hidden side effects that do not alter the visible request or response content (e.g. a silent blind-copy on an email send that never appears in the response) may not be distinguishable from normal use."
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
"id": "AML.T0110.002",
|
|
182
|
+
"limitation": null
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
"id": "AML.T0037",
|
|
186
|
+
"limitation": null
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
"id": "AML.T0055",
|
|
190
|
+
"limitation": "Covers credentials Stroq’s rules recognize by pattern (keys, tokens, known formats) in content a tool call returns; a credential in a format or location the rule set does not recognize will not be flagged."
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
"id": "AML.T0090",
|
|
194
|
+
"limitation": "Covers credential material read through the agent’s own tool calls (files, command output); dumping performed by a separate process outside the agent, with no reflected content, is invisible."
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
"id": "AML.T0035",
|
|
198
|
+
"limitation": "Covers artifact collection performed through the agent’s own file and tool operations; Stroq cannot distinguish routine, legitimate bulk file handling from adversarial collection except by the content and destination involved."
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
"id": "AML.T0126",
|
|
202
|
+
"limitation": "Same limitation as artifact collection: visible as a sequence of ordinary-looking tool calls, which a per-call hook does not aggregate into a pattern of automated collection on its own."
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
"id": "AML.T0025",
|
|
206
|
+
"limitation": "Covers the one channel the corpus currently exercises — a shell-invoked `gh repo create --push` publishing harvested data to a public repository; 'traditional cyber means' also spans exfiltration by direct HTTP request, DNS, email, or cloud-storage upload, none of which any scenario in the suite currently drives through this technique."
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
"id": "AML.T0034.002",
|
|
210
|
+
"limitation": "Stroq can flag an individual costly tool call but does not implement session-level rate-limiting or a budget; a large number of individually unremarkable calls induced across a long session is not aggregated into a single verdict."
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
"id": "AML.T0125",
|
|
214
|
+
"limitation": "Covers account creation performed through the agent’s own tools (an IAM or platform API call); account creation through a channel outside the agent, such as a compromised admin console, is invisible."
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
"id": "AML.T0056",
|
|
218
|
+
"limitation": "Covers the case where the system prompt is extracted by reading a configuration file through a tool call; extraction induced through prompt injection that the model reveals only in its own conversational response is not visible."
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
"id": "AML.T0069",
|
|
222
|
+
"limitation": "Covers system information discovered by reading a file through a tool call; information volunteered by the model when a user simply asks it questions, with no tool call involved, is not visible."
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
"id": "AML.T0069.000",
|
|
226
|
+
"limitation": "Same limitation as the parent: visible only when discovered via a file or tool-call read, not via conversational probing of the model."
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
"id": "AML.T0069.001",
|
|
230
|
+
"limitation": "Same limitation as the parent: visible only when discovered via a file or tool-call read, not via conversational probing of the model."
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
"id": "AML.T0069.002",
|
|
234
|
+
"limitation": "Same limitation as the parent: visible only when the system prompt is read from a configuration file via a tool call, not when extracted through conversation with the model."
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
"id": "AML.T0074",
|
|
238
|
+
"limitation": "Covers file-metadata and type-masquerading content a tool call returns, the same surface as prompt obfuscation; reclaiming a stale, previously-trusted package or model namespace after it is deleted or renamed is adversary-side registry activity outside the developer’s machine and is not covered."
|
|
239
|
+
}
|
|
240
|
+
],
|
|
241
|
+
"outOfScope": [
|
|
242
|
+
{
|
|
243
|
+
"reason": "These techniques interact directly with the model itself — training or querying a proxy, crafting and verifying adversarial or jailbreak inputs, poisoning weights or training data, or inferring and extracting what the model knows via its inference API and conversational responses — and happen through direct model access or the prompt/response channel between a user and the model, not through an agent's tool calls or the content those calls return, which is the only surface a hook observes.",
|
|
244
|
+
"ids": [
|
|
245
|
+
"AML.T0005",
|
|
246
|
+
"AML.T0005.000",
|
|
247
|
+
"AML.T0005.001",
|
|
248
|
+
"AML.T0005.002",
|
|
249
|
+
"AML.T0013",
|
|
250
|
+
"AML.T0014",
|
|
251
|
+
"AML.T0015",
|
|
252
|
+
"AML.T0018",
|
|
253
|
+
"AML.T0018.000",
|
|
254
|
+
"AML.T0018.001",
|
|
255
|
+
"AML.T0018.002",
|
|
256
|
+
"AML.T0018.003",
|
|
257
|
+
"AML.T0020",
|
|
258
|
+
"AML.T0010.002",
|
|
259
|
+
"AML.T0024",
|
|
260
|
+
"AML.T0024.000",
|
|
261
|
+
"AML.T0024.001",
|
|
262
|
+
"AML.T0024.002",
|
|
263
|
+
"AML.T0031",
|
|
264
|
+
"AML.T0040",
|
|
265
|
+
"AML.T0042",
|
|
266
|
+
"AML.T0043",
|
|
267
|
+
"AML.T0043.000",
|
|
268
|
+
"AML.T0043.001",
|
|
269
|
+
"AML.T0043.002",
|
|
270
|
+
"AML.T0043.003",
|
|
271
|
+
"AML.T0043.004",
|
|
272
|
+
"AML.T0044",
|
|
273
|
+
"AML.T0046",
|
|
274
|
+
"AML.T0047",
|
|
275
|
+
"AML.T0054",
|
|
276
|
+
"AML.T0057",
|
|
277
|
+
"AML.T0059",
|
|
278
|
+
"AML.T0061",
|
|
279
|
+
"AML.T0062",
|
|
280
|
+
"AML.T0063",
|
|
281
|
+
"AML.T0065",
|
|
282
|
+
"AML.T0066",
|
|
283
|
+
"AML.T0067",
|
|
284
|
+
"AML.T0067.000",
|
|
285
|
+
"AML.T0076",
|
|
286
|
+
"AML.T0102"
|
|
287
|
+
]
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
"reason": "These are open-source reconnaissance and resource-development activities — searching public research and repositories, scanning or profiling a target, and acquiring infrastructure, accounts, hardware, or attack capabilities — that adversaries carry out on their own systems before ever interacting with the victim's agent, producing no tool call or content for a hook to observe.",
|
|
291
|
+
"ids": [
|
|
292
|
+
"AML.T0000",
|
|
293
|
+
"AML.T0000.000",
|
|
294
|
+
"AML.T0000.001",
|
|
295
|
+
"AML.T0000.002",
|
|
296
|
+
"AML.T0001",
|
|
297
|
+
"AML.T0002",
|
|
298
|
+
"AML.T0002.000",
|
|
299
|
+
"AML.T0002.001",
|
|
300
|
+
"AML.T0002.002",
|
|
301
|
+
"AML.T0003",
|
|
302
|
+
"AML.T0004",
|
|
303
|
+
"AML.T0006",
|
|
304
|
+
"AML.T0007",
|
|
305
|
+
"AML.T0008",
|
|
306
|
+
"AML.T0008.000",
|
|
307
|
+
"AML.T0008.001",
|
|
308
|
+
"AML.T0008.002",
|
|
309
|
+
"AML.T0008.003",
|
|
310
|
+
"AML.T0008.004",
|
|
311
|
+
"AML.T0008.005",
|
|
312
|
+
"AML.T0010.000",
|
|
313
|
+
"AML.T0016",
|
|
314
|
+
"AML.T0016.000",
|
|
315
|
+
"AML.T0016.001",
|
|
316
|
+
"AML.T0016.002",
|
|
317
|
+
"AML.T0016.003",
|
|
318
|
+
"AML.T0016.004",
|
|
319
|
+
"AML.T0017",
|
|
320
|
+
"AML.T0017.000",
|
|
321
|
+
"AML.T0017.001",
|
|
322
|
+
"AML.T0017.002",
|
|
323
|
+
"AML.T0021",
|
|
324
|
+
"AML.T0041",
|
|
325
|
+
"AML.T0064",
|
|
326
|
+
"AML.T0095",
|
|
327
|
+
"AML.T0095.000",
|
|
328
|
+
"AML.T0128"
|
|
329
|
+
]
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
"reason": "These describe publishing, rug-pulling, or building fake reputation for a malicious package, model, dataset, or agent tool on public infrastructure the developer's machine has no view of; the moment Stroq can act is when the agent actually installs or invokes the resulting artifact, which is covered separately under AI Supply Chain Compromise and User Execution.",
|
|
333
|
+
"ids": [
|
|
334
|
+
"AML.T0060",
|
|
335
|
+
"AML.T0079",
|
|
336
|
+
"AML.T0109",
|
|
337
|
+
"AML.T0111",
|
|
338
|
+
"AML.T0115",
|
|
339
|
+
"AML.T0115.000",
|
|
340
|
+
"AML.T0115.001",
|
|
341
|
+
"AML.T0115.002"
|
|
342
|
+
]
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
"reason": "These target a human being directly through phishing, impersonation, deepfakes, or a malicious link a person is persuaded to click, using channels such as email, voice, and video that never pass through an agent's tool calls.",
|
|
346
|
+
"ids": [
|
|
347
|
+
"AML.T0011.003",
|
|
348
|
+
"AML.T0052",
|
|
349
|
+
"AML.T0052.000",
|
|
350
|
+
"AML.T0052.001",
|
|
351
|
+
"AML.T0073",
|
|
352
|
+
"AML.T0087",
|
|
353
|
+
"AML.T0088"
|
|
354
|
+
]
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
"reason": "These are conventional network and infrastructure attacks — exploiting an application or remote service, flooding or overloading a hosted AI service, evading sandboxes, escaping a container, generic credential or defense-evasion exploits, and command-and-control relayed through an AI service's API or web interface — that target infrastructure generally rather than the specific tool calls an agent's hook observes, and remain the domain of conventional network and host security tooling.",
|
|
358
|
+
"ids": [
|
|
359
|
+
"AML.T0036",
|
|
360
|
+
"AML.T0049",
|
|
361
|
+
"AML.T0072",
|
|
362
|
+
"AML.T0089",
|
|
363
|
+
"AML.T0096",
|
|
364
|
+
"AML.T0097",
|
|
365
|
+
"AML.T0105",
|
|
366
|
+
"AML.T0106",
|
|
367
|
+
"AML.T0107",
|
|
368
|
+
"AML.T0114",
|
|
369
|
+
"AML.T0119",
|
|
370
|
+
"AML.T0120",
|
|
371
|
+
"AML.T0122",
|
|
372
|
+
"AML.T0127",
|
|
373
|
+
"AML.T0029",
|
|
374
|
+
"AML.T0034",
|
|
375
|
+
"AML.T0034.000",
|
|
376
|
+
"AML.T0034.001",
|
|
377
|
+
"AML.T0075"
|
|
378
|
+
]
|
|
379
|
+
},
|
|
380
|
+
{
|
|
381
|
+
"reason": "These classify the outcome of a successful attack — financial, reputational, societal, or user harm, intellectual property theft, or full compromise of a machine via its local agent — rather than describing a distinct observable action; the actions that produce these outcomes are addressed separately under their own techniques.",
|
|
382
|
+
"ids": [
|
|
383
|
+
"AML.T0048",
|
|
384
|
+
"AML.T0048.000",
|
|
385
|
+
"AML.T0048.001",
|
|
386
|
+
"AML.T0048.002",
|
|
387
|
+
"AML.T0048.003",
|
|
388
|
+
"AML.T0048.004",
|
|
389
|
+
"AML.T0112",
|
|
390
|
+
"AML.T0112.000",
|
|
391
|
+
"AML.T0112.001"
|
|
392
|
+
]
|
|
393
|
+
},
|
|
394
|
+
{
|
|
395
|
+
"reason": "These exploit a chat client's or webpage's rendering layer — an auto-loaded markdown image that exfiltrates data through its URL, or on-screen content baiting a computer-using agent into clicking or copying something — which a hook inspecting structured tool calls and their text content does not parse.",
|
|
396
|
+
"ids": [
|
|
397
|
+
"AML.T0077",
|
|
398
|
+
"AML.T0100"
|
|
399
|
+
]
|
|
400
|
+
},
|
|
401
|
+
{
|
|
402
|
+
"reason": "These use a stolen session cookie or access token directly against the AI service's backend, or edit chat history through that same access, bypassing the coding agent entirely rather than acting through its tool calls.",
|
|
403
|
+
"ids": [
|
|
404
|
+
"AML.T0091",
|
|
405
|
+
"AML.T0091.000",
|
|
406
|
+
"AML.T0091.001",
|
|
407
|
+
"AML.T0092",
|
|
408
|
+
"AML.T0113",
|
|
409
|
+
"AML.T0012"
|
|
410
|
+
]
|
|
411
|
+
},
|
|
412
|
+
{
|
|
413
|
+
"reason": "These describe coordinated behavior across one or many autonomous agent runs — independent reconnaissance, attack-path replanning, inter-agent communication, environment reconstruction, or orchestration — as an emergent property of a whole run rather than any single tool call; Stroq evaluates one action at a time and does not mediate coordination between agents today.",
|
|
414
|
+
"ids": [
|
|
415
|
+
"AML.T0103",
|
|
416
|
+
"AML.T0116",
|
|
417
|
+
"AML.T0117",
|
|
418
|
+
"AML.T0118",
|
|
419
|
+
"AML.T0118.000",
|
|
420
|
+
"AML.T0118.001",
|
|
421
|
+
"AML.T0121",
|
|
422
|
+
"AML.T0124"
|
|
423
|
+
]
|
|
424
|
+
}
|
|
425
|
+
]
|
|
426
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Stroq plugin for OpenClaw
|
|
2
|
+
|
|
3
|
+
A thin gate that forwards every tool call to the Stroq CLI and does what it says.
|
|
4
|
+
It is shipped inside `@stroq/cli`; `stroq init --agent openclaw` copies it to
|
|
5
|
+
`$STROQ_HOME/openclaw-plugin/` (default `~/.stroq/openclaw-plugin/`), writes a
|
|
6
|
+
`stroq.json` recording how to start Stroq, and then runs — or prints — these two:
|
|
7
|
+
|
|
8
|
+
openclaw plugins install --link ~/.stroq/openclaw-plugin
|
|
9
|
+
openclaw plugins enable stroq
|
|
10
|
+
|
|
11
|
+
Restart the Gateway afterwards: plugins are loaded when it starts.
|
|
12
|
+
|
|
13
|
+
## Hooks
|
|
14
|
+
|
|
15
|
+
- `before_tool_call` (priority 100, no matcher) runs `stroq hook openclaw pre`.
|
|
16
|
+
`allow` returns nothing, `deny` blocks with the rule and reason, `ask` raises a
|
|
17
|
+
`requireApproval` prompt answered with `/approve <id> allow-once|deny`.
|
|
18
|
+
- `after_tool_call` runs `stroq hook openclaw post`, which scans the result, records
|
|
19
|
+
provenance and taints the session. It is observe-only: the warning is logged, the
|
|
20
|
+
taint is enforced on the next tool call.
|
|
21
|
+
|
|
22
|
+
**Fail-closed.** Every one of these blocks the call, which is OpenClaw's own policy
|
|
23
|
+
for this hook: a missing binary, a spawn error, a non-zero exit, a timeout, an aborted
|
|
24
|
+
run, a reply larger than 1 MiB (a hung CLI, not a decision), params that cannot be
|
|
25
|
+
serialised, stdout that is not JSON or is JSON but not an object, and a decision this
|
|
26
|
+
plugin does not know. `after_tool_call` never throws — the tool has already run — and
|
|
27
|
+
a scan that fails there is logged at `warn`, since a failed scan means no taint.
|
|
28
|
+
|
|
29
|
+
If the `stroq.json` `init` recorded points at an entry that no longer exists (an npx
|
|
30
|
+
cache that has since been pruned), the plugin logs one warning and falls back to
|
|
31
|
+
`stroq` on PATH rather than blocking every call on the same ENOENT. When there is no
|
|
32
|
+
Stroq on PATH either, the call still fails closed.
|
|
33
|
+
|
|
34
|
+
## Configuration
|
|
35
|
+
|
|
36
|
+
`plugins.entries.stroq.config` in `openclaw.json`. It is read **once**, when the
|
|
37
|
+
plugin registers, so restart the Gateway after changing any of it:
|
|
38
|
+
|
|
39
|
+
| Key | Default | What it does |
|
|
40
|
+
| -------------- | ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
41
|
+
| `stroqBin` | `stroq.json`, else `stroq` on PATH | Path to the Stroq binary to spawn, as one string (never split on spaces, and never existence-checked — a binary you named must fail loudly rather than be silently replaced). `STROQ_BIN` is read before `stroq.json`, and a `stroq.json` whose recorded entry file is gone is skipped in favour of PATH. A launch command needing extra arguments belongs in `stroq.json`'s `command` array instead. |
|
|
42
|
+
| `workspace` | the Gateway's `process.cwd()` | The project directory for the secret index and path rules. A tool call's own `params.cwd` is never used for this, for any tool including `exec`: honouring one would let an agent point it at an empty directory and slip past a secret-egress guard. |
|
|
43
|
+
| `timeoutMs` | `10000` | How long Stroq gets to answer before the call is blocked. |
|
|
44
|
+
| `askTimeoutMs` | `120000` | How long an approval prompt stays open, clamped to OpenClaw's documented 1 000–600 000 ms range. |
|
|
45
|
+
| `logLevel` | the Gateway's own | Declared for the plugin manager's UI; this entry logs at fixed levels (`warn` for a block, a suspect result, a failed post scan or a stale `stroq.json`, `info` for an approval resolution) and lets the Gateway's logger filter them. |
|
|
46
|
+
|
|
47
|
+
Set `plugins.entries.stroq.enabled` to `true`, and add `stroq` to `plugins.allow` if
|
|
48
|
+
an allowlist is configured. This directory has no dependencies and is plain ESM
|
|
49
|
+
JavaScript, so nothing is installed or built when it is linked.
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// Stroq plugin for OpenClaw: turns `before_tool_call` / `after_tool_call` into
|
|
2
|
+
// `stroq hook openclaw pre|post` child-process calls (spawning is in run-stroq.js).
|
|
3
|
+
// Fail-closed by construction — every one of these blocks the call: a missing binary,
|
|
4
|
+
// a spawn error, a non-zero exit, a timeout, an aborted run, a reply larger than
|
|
5
|
+
// 1 MiB, params that cannot be serialised, stdout that is not JSON or is JSON but not
|
|
6
|
+
// an object, and a decision this file does not know.
|
|
7
|
+
import { createRequire } from 'node:module';
|
|
8
|
+
import { clip, runStroq, text } from './run-stroq.js';
|
|
9
|
+
|
|
10
|
+
const DESCRIPTION =
|
|
11
|
+
'Local action firewall for OpenClaw: scans what the agent reads, taints the session, blocks or asks before dangerous tool calls.';
|
|
12
|
+
const DEFAULT_ASK_TIMEOUT_MS = 120000;
|
|
13
|
+
// OpenClaw's own documented bounds for an approval prompt. A `timeoutMs` past the
|
|
14
|
+
// maximum is not a longer prompt, it is a `requireApproval` the Gateway may reject
|
|
15
|
+
// outright — and a rejected approval is a call nobody was ever asked about.
|
|
16
|
+
const MIN_ASK_TIMEOUT_MS = 1000;
|
|
17
|
+
const MAX_ASK_TIMEOUT_MS = 600000;
|
|
18
|
+
const MAX_TITLE = 80;
|
|
19
|
+
const MAX_DESCRIPTION = 512;
|
|
20
|
+
const load = createRequire(import.meta.url);
|
|
21
|
+
|
|
22
|
+
/** `definePluginEntry`, from whichever SDK path this Gateway build exposes. */
|
|
23
|
+
function resolveDefinePluginEntry() {
|
|
24
|
+
for (const id of ['openclaw/plugin-sdk/plugin-entry', 'openclaw/plugin-sdk/core']) {
|
|
25
|
+
try {
|
|
26
|
+
const mod = load(id);
|
|
27
|
+
const fn = mod?.definePluginEntry ?? mod?.default?.definePluginEntry;
|
|
28
|
+
if (typeof fn === 'function') return fn;
|
|
29
|
+
} catch {
|
|
30
|
+
// not exposed by this build; try the next id, then fall back to bare `register`
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Logging never decides anything: an absent logger is skipped and a throwing one is swallowed. */
|
|
37
|
+
function logAt(api, level, message) {
|
|
38
|
+
const fn = api && api.logger && api.logger[level];
|
|
39
|
+
try {
|
|
40
|
+
if (typeof fn === 'function') fn.call(api.logger, message);
|
|
41
|
+
} catch {}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Both phases' payload. `cwd` is always the plugin's OWN directory — `config.workspace`,
|
|
46
|
+
* else `process.cwd()` — and never a tool call's `params.cwd`: honouring a
|
|
47
|
+
* model-supplied `cwd` here would let any tool point the project directory (and so
|
|
48
|
+
* the secret index) at an empty one and walk straight past a secret-egress guard.
|
|
49
|
+
* `exec` is no exception: the CLI adapter also never reads `params.cwd` for this
|
|
50
|
+
* (Task 4.5 review, Critical — it used to, and that was the actual bypass). `params`
|
|
51
|
+
* is still forwarded whole regardless, `cwd` included, purely for the audit trail.
|
|
52
|
+
*/
|
|
53
|
+
function payloadFor(phase, event, ctx, config) {
|
|
54
|
+
const params = event.params && typeof event.params === 'object' ? event.params : {};
|
|
55
|
+
const c = ctx || {};
|
|
56
|
+
const base = {
|
|
57
|
+
sessionId: text(c.sessionKey) || text(c.sessionId) || 'openclaw',
|
|
58
|
+
agentId: c.agentId,
|
|
59
|
+
runId: c.runId ?? event.runId,
|
|
60
|
+
toolCallId: c.toolCallId ?? event.toolCallId,
|
|
61
|
+
toolKind: event.toolKind ?? c.toolKind,
|
|
62
|
+
requester: c.requester,
|
|
63
|
+
toolName: text(event.toolName),
|
|
64
|
+
params,
|
|
65
|
+
cwd: text(config.workspace) || process.cwd(),
|
|
66
|
+
};
|
|
67
|
+
if (phase === 'pre') return base;
|
|
68
|
+
return { ...base, result: event.result, error: event.error, durationMs: event.durationMs };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** How long the prompt stays open, clamped to the range OpenClaw documents. */
|
|
72
|
+
function askTimeout(config) {
|
|
73
|
+
const configured = Number(config.askTimeoutMs);
|
|
74
|
+
if (!(configured > 0)) return DEFAULT_ASK_TIMEOUT_MS;
|
|
75
|
+
return Math.min(Math.max(configured, MIN_ASK_TIMEOUT_MS), MAX_ASK_TIMEOUT_MS);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** `ask` as OpenClaw's approval request, inside its documented 80/512/600 000 caps. */
|
|
79
|
+
function approval(api, event, reply, config) {
|
|
80
|
+
const ms = askTimeout(config);
|
|
81
|
+
return {
|
|
82
|
+
title: clip(`Stroq: ${text(reply.ruleId) || 'policy'}`, MAX_TITLE),
|
|
83
|
+
description: clip(text(reply.reason) || 'Stroq asks before this action.', MAX_DESCRIPTION),
|
|
84
|
+
severity: 'warning',
|
|
85
|
+
// `allow-always` is deliberately not offered: Stroq audits every ask, and a
|
|
86
|
+
// remembered allow is one it would never be asked about again.
|
|
87
|
+
allowedDecisions: ['allow-once', 'deny'],
|
|
88
|
+
timeoutMs: ms,
|
|
89
|
+
onResolution: (decision) =>
|
|
90
|
+
logAt(api, 'info', `stroq: approval ${decision} for ${text(event.toolName)}`),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function register(api) {
|
|
95
|
+
const config = (api && api.pluginConfig) || {};
|
|
96
|
+
// `detail` is clipped like a child's stderr: it is a block reason shown to a user.
|
|
97
|
+
const block = (event, detail) => {
|
|
98
|
+
const clipped = clip(String(detail), 300);
|
|
99
|
+
logAt(api, 'warn', `stroq: ${text(event && event.toolName) || 'tool'}: ${clipped}`);
|
|
100
|
+
return { block: true, blockReason: `Stroq internal error (fail-closed): ${clipped}` };
|
|
101
|
+
};
|
|
102
|
+
// A stale `stroq.json` entry is reported at `warn`: the operator should repair it.
|
|
103
|
+
const warn = (message) => logAt(api, 'warn', `stroq: ${message}`);
|
|
104
|
+
// Priority 100 so Stroq answers before ordinary hooks; no matcher, every tool goes through.
|
|
105
|
+
api.on(
|
|
106
|
+
'before_tool_call',
|
|
107
|
+
async (event, ctx) => {
|
|
108
|
+
let outcome;
|
|
109
|
+
try {
|
|
110
|
+
const payload = payloadFor('pre', event, ctx, config);
|
|
111
|
+
outcome = await runStroq(config, 'pre', payload, ctx?.abortSignal, warn);
|
|
112
|
+
} catch (err) {
|
|
113
|
+
return block(event, `cannot read the tool call: ${String(err)}`);
|
|
114
|
+
}
|
|
115
|
+
if (outcome.error) return block(event, outcome.error);
|
|
116
|
+
const reply = outcome.reply;
|
|
117
|
+
if (reply.decision === 'allow') return undefined;
|
|
118
|
+
if (reply.decision === 'ask') return { requireApproval: approval(api, event, reply, config) };
|
|
119
|
+
if (reply.decision === 'deny')
|
|
120
|
+
return {
|
|
121
|
+
block: true,
|
|
122
|
+
blockReason: `Stroq blocked this action (${text(reply.ruleId) || 'policy'}): ${text(reply.reason) || 'no reason given'}`,
|
|
123
|
+
};
|
|
124
|
+
return block(event, `unknown decision ${JSON.stringify(reply.decision)}`);
|
|
125
|
+
},
|
|
126
|
+
{ priority: 100 },
|
|
127
|
+
);
|
|
128
|
+
// Observe-only and never throws; the taint it sets is enforced on the NEXT call. The
|
|
129
|
+
// abort signal is not forwarded: a cancelled run's result must still be scanned.
|
|
130
|
+
api.on('after_tool_call', async (event, ctx) => {
|
|
131
|
+
try {
|
|
132
|
+
const payload = payloadFor('post', event, ctx, config);
|
|
133
|
+
const outcome = await runStroq(config, 'post', payload, undefined, warn);
|
|
134
|
+
// `warn`: a scan nobody looked at leaves the session untainted; operators must see it.
|
|
135
|
+
if (outcome.error) logAt(api, 'warn', `stroq: post scan failed: ${outcome.error}`);
|
|
136
|
+
else if (text(outcome.reply.warning)) logAt(api, 'warn', `stroq: ${outcome.reply.warning}`);
|
|
137
|
+
} catch (err) {
|
|
138
|
+
logAt(api, 'warn', `stroq: post scan failed: ${String(err)}`);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const definePluginEntry = resolveDefinePluginEntry();
|
|
144
|
+
export default definePluginEntry
|
|
145
|
+
? definePluginEntry({ id: 'stroq', name: 'Stroq', description: DESCRIPTION, register })
|
|
146
|
+
: register;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "stroq",
|
|
3
|
+
"name": "Stroq",
|
|
4
|
+
"description": "Local action firewall for OpenClaw: scans what the agent reads, taints the session, blocks or asks before dangerous tool calls.",
|
|
5
|
+
"version": "0.11.0",
|
|
6
|
+
"configSchema": {
|
|
7
|
+
"type": "object",
|
|
8
|
+
"additionalProperties": false,
|
|
9
|
+
"properties": {
|
|
10
|
+
"stroqBin": { "type": "string" },
|
|
11
|
+
"workspace": { "type": "string" },
|
|
12
|
+
"timeoutMs": { "type": "integer", "minimum": 1000 },
|
|
13
|
+
"askTimeoutMs": { "type": "integer", "minimum": 1000 },
|
|
14
|
+
"logLevel": { "type": "string", "enum": ["warn", "info", "debug"] }
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"uiHints": {
|
|
18
|
+
"stroqBin": {
|
|
19
|
+
"label": "stroq command",
|
|
20
|
+
"help": "Path to the stroq binary. Defaults to `stroq` on PATH, then the path recorded by `stroq init --agent openclaw`."
|
|
21
|
+
},
|
|
22
|
+
"workspace": {
|
|
23
|
+
"label": "Project directory",
|
|
24
|
+
"help": "Used for the secret index and path classification. A tool call's own cwd is never used for this — see the plugin README."
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|