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
//! yana-rt WASM library
//!
//! Exposes core guard logic as WebAssembly for use in browsers,
//! VS Code extensions, and other JS environments.
//!
//! Build: `wasm-pack build --target web --features wasm`
#[cfg(feature = "wasm")]
mod wasm {
use wasm_bindgen::prelude::*;
// ── Destructive command patterns (mirrors guard/mod.rs) ──────────────────
struct Pattern {
re: regex::Regex,
reason: &'static str,
}
fn destructive_patterns() -> Vec<Pattern> {
let rules: &[(&str, &str)] = &[
(
r"(?i)(^|[;&|])\s*rm\s+-[a-zA-Z]*r[a-zA-Z]*f|rm\s+-[a-zA-Z]*f[a-zA-Z]*r",
"Blocked: 'rm -rf' is irreversible. Use targeted 'rm' with explicit paths.",
),
(
r"(?i)git\s+push\s+.*--force|git\s+push\s+.*-f\b",
"Blocked: force-push can destroy shared history. Requires explicit approval.",
),
(
r"(?i)git\s+reset\s+--hard",
"Blocked: 'git reset --hard' discards all uncommitted changes permanently.",
),
(
r"(?i)(curl|wget)\s+.*\|\s*(ba)?sh",
"Blocked: pipe-to-shell is a supply chain attack vector.",
),
(
r"(?i)DROP\s+(TABLE|DATABASE|SCHEMA)\s+",
"Blocked: DDL DROP is irreversible without a prior backup.",
),
(
r"(?i)npm\s+publish\b",
"Blocked: 'npm publish' requires explicit human gate approval.",
),
(
r"(?i)git\s+push\s+.*origin\s+main|git\s+push\s+.*origin\s+master",
"Blocked: push to main/master requires explicit authorization.",
),
];
rules
.iter()
.filter_map(|(pat, reason)| {
regex::Regex::new(pat).ok().map(|re| Pattern { re, reason })
})
.collect()
}
// ── Exported functions ────────────────────────────────────────────────────
/// Check whether a shell command is safe to execute.
///
/// Input: raw command string
/// Output: JSON `{"allowed": bool, "reason": string | null}`
///
/// ```js
/// import init, { check_command } from './pkg/yana_rt.js';
/// await init();
/// const result = JSON.parse(check_command('rm -rf /'));
/// // → { allowed: false, reason: "Blocked: 'rm -rf' is irreversible..." }
/// ```
#[wasm_bindgen]
pub fn check_command(cmd: &str) -> String {
for p in &destructive_patterns() {
if p.re.is_match(cmd) {
return serde_json::json!({
"allowed": false,
"reason": p.reason
})
.to_string();
}
}
serde_json::json!({ "allowed": true, "reason": null }).to_string()
}
/// Batch-check a JSON array of command strings.
///
/// Input: JSON string — array of command strings
/// Output: JSON array of `{cmd, allowed, reason}` objects
///
/// ```js
/// const results = JSON.parse(check_commands('["ls", "rm -rf /"]'));
/// ```
#[wasm_bindgen]
pub fn check_commands(cmds_json: &str) -> String {
let cmds: Vec<String> = match serde_json::from_str(cmds_json) {
Ok(v) => v,
Err(e) => {
return serde_json::json!({
"error": format!("invalid JSON: {e}")
})
.to_string()
}
};
let results: Vec<serde_json::Value> = cmds
.iter()
.map(|cmd| {
for p in &destructive_patterns() {
if p.re.is_match(cmd) {
return serde_json::json!({
"cmd": cmd,
"allowed": false,
"reason": p.reason
});
}
}
serde_json::json!({ "cmd": cmd, "allowed": true, "reason": null })
})
.collect();
serde_json::to_string(&results).unwrap_or_default()
}
/// Returns library version string.
#[wasm_bindgen]
pub fn version() -> String {
env!("CARGO_PKG_VERSION").to_string()
}
}