act-cli 0.5.0__tar.gz → 0.5.1__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.
- {act_cli-0.5.0 → act_cli-0.5.1}/Cargo.lock +2 -2
- {act_cli-0.5.0 → act_cli-0.5.1}/Cargo.toml +1 -1
- {act_cli-0.5.0 → act_cli-0.5.1}/PKG-INFO +1 -1
- {act_cli-0.5.0 → act_cli-0.5.1}/act-cli/src/runtime/fs_matcher.rs +122 -6
- {act_cli-0.5.0 → act_cli-0.5.1}/README.md +0 -0
- {act_cli-0.5.0 → act_cli-0.5.1}/act-cli/Cargo.toml +0 -0
- {act_cli-0.5.0 → act_cli-0.5.1}/act-cli/README.md +0 -0
- {act_cli-0.5.0 → act_cli-0.5.1}/act-cli/build.rs +0 -0
- {act_cli-0.5.0 → act_cli-0.5.1}/act-cli/src/config.rs +0 -0
- {act_cli-0.5.0 → act_cli-0.5.1}/act-cli/src/format.rs +0 -0
- {act_cli-0.5.0 → act_cli-0.5.1}/act-cli/src/http.rs +0 -0
- {act_cli-0.5.0 → act_cli-0.5.1}/act-cli/src/main.rs +0 -0
- {act_cli-0.5.0 → act_cli-0.5.1}/act-cli/src/mcp.rs +0 -0
- {act_cli-0.5.0 → act_cli-0.5.1}/act-cli/src/resolve.rs +0 -0
- {act_cli-0.5.0 → act_cli-0.5.1}/act-cli/src/runtime/bindings/mod.rs +0 -0
- {act_cli-0.5.0 → act_cli-0.5.1}/act-cli/src/runtime/effective.rs +0 -0
- {act_cli-0.5.0 → act_cli-0.5.1}/act-cli/src/runtime/fs_policy.rs +0 -0
- {act_cli-0.5.0 → act_cli-0.5.1}/act-cli/src/runtime/http_client.rs +0 -0
- {act_cli-0.5.0 → act_cli-0.5.1}/act-cli/src/runtime/http_policy.rs +0 -0
- {act_cli-0.5.0 → act_cli-0.5.1}/act-cli/src/runtime/mod.rs +0 -0
- {act_cli-0.5.0 → act_cli-0.5.1}/act-cli/src/runtime/network.rs +0 -0
- {act_cli-0.5.0 → act_cli-0.5.1}/act-cli/wit/deps/act-core/act-core.wit +0 -0
- {act_cli-0.5.0 → act_cli-0.5.1}/act-cli/wit/deps/act-core/act-events.wit +0 -0
- {act_cli-0.5.0 → act_cli-0.5.1}/act-cli/wit/deps/act-core/act-resources.wit +0 -0
- {act_cli-0.5.0 → act_cli-0.5.1}/act-cli/wit/deps.lock +0 -0
- {act_cli-0.5.0 → act_cli-0.5.1}/act-cli/wit/deps.toml +0 -0
- {act_cli-0.5.0 → act_cli-0.5.1}/act-cli/wit/world.wit +0 -0
- {act_cli-0.5.0 → act_cli-0.5.1}/pyproject.toml +0 -0
|
@@ -4,7 +4,7 @@ version = 4
|
|
|
4
4
|
|
|
5
5
|
[[package]]
|
|
6
6
|
name = "act-build"
|
|
7
|
-
version = "0.5.
|
|
7
|
+
version = "0.5.1"
|
|
8
8
|
dependencies = [
|
|
9
9
|
"act-types",
|
|
10
10
|
"anyhow",
|
|
@@ -25,7 +25,7 @@ dependencies = [
|
|
|
25
25
|
|
|
26
26
|
[[package]]
|
|
27
27
|
name = "act-cli"
|
|
28
|
-
version = "0.5.
|
|
28
|
+
version = "0.5.1"
|
|
29
29
|
dependencies = [
|
|
30
30
|
"act-types",
|
|
31
31
|
"anyhow",
|
|
@@ -18,10 +18,19 @@
|
|
|
18
18
|
//! Decision rule:
|
|
19
19
|
//! - Mode = Deny → always `Deny`.
|
|
20
20
|
//! - Mode = Open → always `Allow`.
|
|
21
|
-
//! - Mode = Allowlist
|
|
22
|
-
//! if any
|
|
21
|
+
//! - Mode = Allowlist:
|
|
22
|
+
//! - `Deny` if any deny pattern matches.
|
|
23
|
+
//! - `Allow` if any allow pattern matches.
|
|
24
|
+
//! - `Allow` if the path is a directory **ancestor** of any allowed
|
|
25
|
+
//! pattern's literal prefix. WASI path resolution stats every
|
|
26
|
+
//! intermediate directory when opening a nested path, so a user
|
|
27
|
+
//! granting `/tmp/work/db.sqlite` implicitly grants traversal on
|
|
28
|
+
//! `/tmp/work` and `/tmp` (metadata only — those dirs aren't
|
|
29
|
+
//! "allowed" for listing, but they are for the traversal needed
|
|
30
|
+
//! to reach the target).
|
|
31
|
+
//! - `Deny` otherwise.
|
|
23
32
|
|
|
24
|
-
use std::path::Path;
|
|
33
|
+
use std::path::{Path, PathBuf};
|
|
25
34
|
|
|
26
35
|
use anyhow::{Context, Result};
|
|
27
36
|
use globset::{Glob, GlobSet, GlobSetBuilder};
|
|
@@ -43,15 +52,27 @@ pub struct FsMatcher {
|
|
|
43
52
|
mode: PolicyMode,
|
|
44
53
|
allow: GlobSet,
|
|
45
54
|
deny: GlobSet,
|
|
55
|
+
/// Literal path prefix of each allow entry — the longest ancestor
|
|
56
|
+
/// with no glob metacharacter. `/a/b/c.db` → `/a/b/c.db`;
|
|
57
|
+
/// `/tmp/*.db` → `/tmp`; `/foo/bar/**` → `/foo/bar`. Used to permit
|
|
58
|
+
/// traversal of intermediate directories on the path to any
|
|
59
|
+
/// allowed target.
|
|
60
|
+
allow_prefixes: Vec<PathBuf>,
|
|
46
61
|
}
|
|
47
62
|
|
|
48
63
|
impl FsMatcher {
|
|
49
64
|
/// Compile a matcher from a resolved `FsConfig`.
|
|
50
65
|
pub fn compile(cfg: &FsConfig) -> Result<Self> {
|
|
66
|
+
let mut allow_prefixes = Vec::new();
|
|
67
|
+
for pat in &cfg.allow {
|
|
68
|
+
let expanded = expand_pattern(pat);
|
|
69
|
+
allow_prefixes.push(PathBuf::from(literal_prefix(&expanded)));
|
|
70
|
+
}
|
|
51
71
|
Ok(Self {
|
|
52
72
|
mode: cfg.mode,
|
|
53
73
|
allow: compile_set("allow", &cfg.allow)?,
|
|
54
74
|
deny: compile_set("deny", &cfg.deny)?,
|
|
75
|
+
allow_prefixes,
|
|
55
76
|
})
|
|
56
77
|
}
|
|
57
78
|
|
|
@@ -65,15 +86,60 @@ impl FsMatcher {
|
|
|
65
86
|
return FsDecision::Deny;
|
|
66
87
|
}
|
|
67
88
|
if self.allow.is_match(path) {
|
|
68
|
-
FsDecision::Allow
|
|
69
|
-
}
|
|
70
|
-
|
|
89
|
+
return FsDecision::Allow;
|
|
90
|
+
}
|
|
91
|
+
// Ancestor-traversal check: allow stat/open on any directory
|
|
92
|
+
// that lies on the path to some allowed target.
|
|
93
|
+
if self
|
|
94
|
+
.allow_prefixes
|
|
95
|
+
.iter()
|
|
96
|
+
.any(|prefix| is_ancestor(path, prefix))
|
|
97
|
+
{
|
|
98
|
+
return FsDecision::Allow;
|
|
71
99
|
}
|
|
100
|
+
FsDecision::Deny
|
|
72
101
|
}
|
|
73
102
|
}
|
|
74
103
|
}
|
|
75
104
|
}
|
|
76
105
|
|
|
106
|
+
/// Extract the longest leading path segment of `pattern` that contains no
|
|
107
|
+
/// glob metacharacter (`*`, `?`, `[`, `{`). That segment is the literal
|
|
108
|
+
/// prefix under which the glob might match.
|
|
109
|
+
fn literal_prefix(pattern: &str) -> &str {
|
|
110
|
+
// Find the first component containing a metachar. Keep everything before it.
|
|
111
|
+
let bytes = pattern.as_bytes();
|
|
112
|
+
let mut last_boundary = 0usize;
|
|
113
|
+
let mut i = 0usize;
|
|
114
|
+
while i < bytes.len() {
|
|
115
|
+
let b = bytes[i];
|
|
116
|
+
if b == b'/' {
|
|
117
|
+
last_boundary = i;
|
|
118
|
+
} else if matches!(b, b'*' | b'?' | b'[' | b'{') {
|
|
119
|
+
return &pattern[..last_boundary];
|
|
120
|
+
}
|
|
121
|
+
i += 1;
|
|
122
|
+
}
|
|
123
|
+
// No metachar found — the whole pattern is literal.
|
|
124
|
+
pattern
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/// Is `candidate` an ancestor of `target` (i.e., `target` is `candidate`
|
|
128
|
+
/// with zero or more additional components)? Works by walking `target`'s
|
|
129
|
+
/// ancestor chain looking for an exact match. Returns `false` if
|
|
130
|
+
/// `candidate` is empty.
|
|
131
|
+
fn is_ancestor(candidate: &Path, target: &Path) -> bool {
|
|
132
|
+
if candidate.as_os_str().is_empty() {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
for ancestor in target.ancestors() {
|
|
136
|
+
if ancestor == candidate {
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
false
|
|
141
|
+
}
|
|
142
|
+
|
|
77
143
|
fn compile_set(label: &str, patterns: &[String]) -> Result<GlobSet> {
|
|
78
144
|
let mut b = GlobSetBuilder::new();
|
|
79
145
|
for p in patterns {
|
|
@@ -213,6 +279,56 @@ mod tests {
|
|
|
213
279
|
);
|
|
214
280
|
}
|
|
215
281
|
|
|
282
|
+
#[test]
|
|
283
|
+
fn ancestor_of_allowed_literal_file_is_traversable() {
|
|
284
|
+
// Allowing /tmp/work/db.sqlite implicitly grants traversal on
|
|
285
|
+
// /tmp/work and /tmp so the WASI path-walker can stat each
|
|
286
|
+
// intermediate directory before reaching the target.
|
|
287
|
+
let m =
|
|
288
|
+
FsMatcher::compile(&cfg(PolicyMode::Allowlist, &["/tmp/work/db.sqlite"], &[])).unwrap();
|
|
289
|
+
assert_eq!(
|
|
290
|
+
m.decide(&PathBuf::from("/tmp/work/db.sqlite")),
|
|
291
|
+
FsDecision::Allow
|
|
292
|
+
);
|
|
293
|
+
assert_eq!(m.decide(&PathBuf::from("/tmp/work")), FsDecision::Allow);
|
|
294
|
+
assert_eq!(m.decide(&PathBuf::from("/tmp")), FsDecision::Allow);
|
|
295
|
+
assert_eq!(m.decide(&PathBuf::from("/")), FsDecision::Allow);
|
|
296
|
+
// Sibling dir not on the path — still denied.
|
|
297
|
+
assert_eq!(m.decide(&PathBuf::from("/tmp/other")), FsDecision::Deny);
|
|
298
|
+
assert_eq!(m.decide(&PathBuf::from("/var")), FsDecision::Deny);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
#[test]
|
|
302
|
+
fn ancestor_of_glob_literal_prefix_is_traversable() {
|
|
303
|
+
let m =
|
|
304
|
+
FsMatcher::compile(&cfg(PolicyMode::Allowlist, &["/tmp/work/**/*.db"], &[])).unwrap();
|
|
305
|
+
// Literal prefix is /tmp/work. Ancestors allowed.
|
|
306
|
+
assert_eq!(m.decide(&PathBuf::from("/tmp/work")), FsDecision::Allow);
|
|
307
|
+
assert_eq!(m.decide(&PathBuf::from("/tmp")), FsDecision::Allow);
|
|
308
|
+
// A .db inside is allowed by the glob.
|
|
309
|
+
assert_eq!(
|
|
310
|
+
m.decide(&PathBuf::from("/tmp/work/a/b.db")),
|
|
311
|
+
FsDecision::Allow
|
|
312
|
+
);
|
|
313
|
+
// Non-.db file below is NOT allowed — ancestor rule only covers
|
|
314
|
+
// *reaching* the allowed target, not reading siblings.
|
|
315
|
+
assert_eq!(
|
|
316
|
+
m.decide(&PathBuf::from("/tmp/work/a/b.txt")),
|
|
317
|
+
FsDecision::Deny
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
#[test]
|
|
322
|
+
fn ancestor_does_not_leak_past_first_glob_component() {
|
|
323
|
+
// `/tmp/*.db` — the literal prefix is `/tmp`. Ancestors of that
|
|
324
|
+
// (i.e. `/`) are traversable, but so is `/tmp` itself. What
|
|
325
|
+
// shouldn't leak: a sibling of the glob target.
|
|
326
|
+
let m = FsMatcher::compile(&cfg(PolicyMode::Allowlist, &["/tmp/*.db"], &[])).unwrap();
|
|
327
|
+
assert_eq!(m.decide(&PathBuf::from("/tmp")), FsDecision::Allow);
|
|
328
|
+
assert_eq!(m.decide(&PathBuf::from("/tmp/foo.db")), FsDecision::Allow);
|
|
329
|
+
assert_eq!(m.decide(&PathBuf::from("/tmp/foo.txt")), FsDecision::Deny);
|
|
330
|
+
}
|
|
331
|
+
|
|
216
332
|
#[test]
|
|
217
333
|
fn extension_glob() {
|
|
218
334
|
let m = FsMatcher::compile(&cfg(PolicyMode::Allowlist, &["/tmp/**/*.md"], &[])).unwrap();
|
|
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
|