1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
//! Integration tests for the plugin registry-file feature (ADR-031, task E5.3).
//!
//! These tests exercise the full CLI process for the adversarial proof scenarios
//! required by E5.3:
//!
//! 1. Registry file forgery attempt — command stored not executed at parse time.
//! 2. Missing required field — entry skipped, other entries still load.
//! 3. Missing registry file — error to stderr, non-zero exit.
//! 4. Static entry wins on name conflict — warning emitted.
//!
//! All tests require the `plugins` feature. Each uses `TSAFE_PLUGIN_REGISTRY`
//! to point at a temp file.
#[cfg(feature = "plugins")]
mod registry_integration {
use assert_cmd::Command;
use predicates::prelude::PredicateBooleanExt;
use predicates::str::contains;
use std::io::Write as _;
use tempfile::tempdir;
fn tsafe() -> Command {
Command::cargo_bin("tsafe").unwrap()
}
fn init_vault(dir: &std::path::Path) {
tsafe()
.args(["init"])
.env("TSAFE_VAULT_DIR", dir)
.env("TSAFE_PASSWORD", "pw")
.assert()
.success();
}
fn write_registry(content: &str) -> tempfile::NamedTempFile {
let mut f = tempfile::NamedTempFile::new().unwrap();
f.write_all(content.as_bytes()).unwrap();
f
}
// ── E5.3-1: Forgery attempt — stored, not executed ─────────────────────
/// A registry entry containing a shell injection in the `command` field is
/// stored literally and never executed at parse/list time. The `tsafe plugin
/// list` command must succeed with exit 0 and display the entry without any
/// side effects from executing the command string.
///
/// This is the adversarial proof: if any code-execution occurred at load
/// time (e.g., the command string was eval'd), this test would hang or fail.
#[test]
fn registry_forgery_command_listed_not_executed_at_load_time() {
let dir = tempdir().unwrap();
init_vault(dir.path());
// A command string that would cause visible side effects if eval'd.
// On any platform, this is a syntax error if run literally — which is
// correct behavior: tsafe must not run it at all during loading.
let registry = r#"
[[plugins]]
name = "evil-tool"
command = "/bin/sh -c 'rm -rf /'"
description = "Adversarial entry"
"#;
let f = write_registry(registry);
// `tsafe plugin list` must succeed and display the entry — no execution.
tsafe()
.args(["plugin", "list"])
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "pw")
.env("TSAFE_PLUGIN_REGISTRY", f.path())
.assert()
.success()
// The entry name appears in the list.
.stdout(contains("evil-tool"))
// It is tagged [registry], not [built-in].
.stdout(contains("[registry]"))
// Static entries still appear.
.stdout(contains("[built-in]"));
}
// ── E5.3-2: Missing required field — entry skipped ─────────────────────
/// An entry missing the `command` field is skipped with a warning to stderr.
/// Other valid entries in the same file continue to load normally.
#[test]
fn registry_entry_missing_command_skipped_others_load() {
let dir = tempdir().unwrap();
init_vault(dir.path());
let registry = r#"
[[plugins]]
name = "bad-no-command"
# command is missing — this entry must be skipped
[[plugins]]
name = "good-tool"
command = "/usr/local/bin/good-tool"
description = "A valid entry"
"#;
let f = write_registry(registry);
let assert = tsafe()
.args(["plugin", "list"])
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "pw")
.env("TSAFE_PLUGIN_REGISTRY", f.path())
.assert()
.success();
// The good entry loads.
assert.stdout(contains("good-tool"));
// The bad entry is NOT listed (it was skipped).
// We can check stderr for the warning.
// Note: `assert` has been consumed; re-run to check stderr separately.
tsafe()
.args(["plugin", "list"])
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "pw")
.env("TSAFE_PLUGIN_REGISTRY", f.path())
.assert()
.success()
.stderr(contains("skipping"))
.stderr(contains("command"));
}
/// An entry missing the `name` field is also skipped with a warning.
#[test]
fn registry_entry_missing_name_skipped() {
let dir = tempdir().unwrap();
init_vault(dir.path());
let registry = r#"
[[plugins]]
command = "/usr/local/bin/nameless"
# name is missing
[[plugins]]
name = "named-tool"
command = "/usr/local/bin/named-tool"
"#;
let f = write_registry(registry);
tsafe()
.args(["plugin", "list"])
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "pw")
.env("TSAFE_PLUGIN_REGISTRY", f.path())
.assert()
.success()
.stdout(contains("named-tool"))
.stderr(contains("skipping"))
.stderr(contains("name"));
}
// ── E5.3-3: Missing registry file → error, non-zero exit ───────────────
/// When `TSAFE_PLUGIN_REGISTRY` is set to a non-existent path, tsafe must
/// exit non-zero and print an error mentioning the registry problem.
/// The error path must not silently succeed.
#[test]
fn missing_registry_file_exits_nonzero_with_error() {
let dir = tempdir().unwrap();
init_vault(dir.path());
tsafe()
.args(["plugin", "list"])
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "pw")
.env("TSAFE_PLUGIN_REGISTRY", "/nonexistent/path/to/plugins.toml")
.assert()
.failure()
.stderr(contains("plugin registry"));
}
// ── E5.3-4: Static entry wins on name conflict ──────────────────────────
/// When a registry entry has the same `name` as a static built-in entry,
/// the static entry wins. The registry entry does NOT shadow the built-in.
/// A warning must be emitted to inform the operator.
#[test]
fn static_entry_wins_on_name_conflict_warning_emitted() {
let dir = tempdir().unwrap();
init_vault(dir.path());
// "gh" is a static built-in. A registry entry with the same name must
// not override it.
let registry = r#"
[[plugins]]
name = "gh"
command = "/attacker/fake-gh"
description = "Shadowing attempt"
"#;
let f = write_registry(registry);
// The list command must succeed, show gh as [built-in], and emit a
// warning about the conflict.
tsafe()
.args(["plugin", "list"])
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "pw")
.env("TSAFE_PLUGIN_REGISTRY", f.path())
.assert()
.success()
// gh appears (from static)
.stdout(contains("gh"))
// Warning about the conflict — must mention the tool name and conflict
.stderr(contains("gh"))
.stderr(
contains("built-in")
.or(contains("conflict"))
.or(contains("static")),
);
}
// ── Registry entry with optional fields ────────────────────────────────
/// Optional fields (description, args, url) are shown correctly in list.
#[test]
fn registry_entry_with_optional_fields_shown_in_list() {
let dir = tempdir().unwrap();
init_vault(dir.path());
let registry = r#"
[[plugins]]
name = "my-tool"
command = "/usr/local/bin/my-tool"
description = "My custom operator tool"
url = "https://example.com/my-tool"
args = ["--tsafe-mode"]
"#;
let f = write_registry(registry);
tsafe()
.args(["plugin", "list"])
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "pw")
.env("TSAFE_PLUGIN_REGISTRY", f.path())
.assert()
.success()
.stdout(contains("my-tool"))
.stdout(contains("My custom operator tool"))
.stdout(contains("[registry]"));
}
// ── No registry env var → opt-in only ──────────────────────────────────
/// When `TSAFE_PLUGIN_REGISTRY` is not set, the plugin list shows only
/// static built-ins. No implicit path is searched.
#[test]
fn no_registry_env_var_shows_only_builtins() {
let dir = tempdir().unwrap();
init_vault(dir.path());
tsafe()
.args(["plugin", "list"])
.env("TSAFE_VAULT_DIR", dir.path())
.env("TSAFE_PASSWORD", "pw")
.env_remove("TSAFE_PLUGIN_REGISTRY")
.assert()
.success()
.stdout(contains("[built-in]"))
// No [registry] tag when no registry is configured.
.stdout(contains("[registry]").not());
}
}