ps_parser/
lib.rs

1//! # ps-parser
2//!
3//! A fast and flexible PowerShell parser written in Rust.
4//!
5//! ## Overview
6//!
7//! `ps-parser` provides parsing, evaluation, and manipulation of PowerShell
8//! scripts. It supports variables, arrays, hash tables, script blocks,
9//! arithmetic, logical operations, and more.
10//!
11//! ## Features
12//!
13//! - Parse PowerShell scripts using pest grammar
14//! - Evaluate expressions, variables, arrays, hash tables, and script blocks
15//! - Environment and INI variable loading
16//! - Deobfuscation and error reporting
17//! - Extensible for custom PowerShell types
18//!
19//! ## Usage
20//!
21//! ```rust
22//! use ps_parser::PowerShellSession;
23//!
24//! let mut session = PowerShellSession::new();
25//! let output = session.safe_eval(r#"$a = 42; Write-Output $a"#).unwrap();
26//! println!("{}", output); // prints: 42
27//! ```
28
29mod parser;
30
31pub(crate) use parser::NEWLINE;
32/// Represents a PowerShell parsing and evaluation session.
33///
34/// This is the main entry point for parsing and evaluating PowerShell scripts.
35/// It maintains the session state including variables, tokens, and error
36/// information.
37///
38/// # Examples
39///
40/// ```rust
41/// use ps_parser::PowerShellSession;
42///
43/// // Create a new session
44/// let mut session = PowerShellSession::new();
45///
46/// // Evaluate a simple expression
47/// let result = session.safe_eval("$a = 1 + 2; Write-Output $a").unwrap();
48/// assert_eq!(result, "3");
49///
50/// // Parse and get detailed results
51/// let script_result = session.parse_input("$b = 'Hello World'; $b").unwrap();
52/// println!("Result: {:?}", script_result.result());
53/// ```
54///
55/// Represents a PowerShell value that can be stored and manipulated.
56///
57/// This enum covers all the basic PowerShell data types including primitives,
58/// collections, and complex objects like script blocks and hash tables.
59///
60/// # Examples
61///
62/// ```rust
63/// use ps_parser::PsValue;
64///
65/// // Different value types  
66/// let int_val = PsValue::Int(42);
67/// let string_val = PsValue::String("Hello".into());
68/// let bool_val = PsValue::Bool(true);
69/// ```
70///
71/// Contains the complete result of parsing and evaluating a PowerShell script.
72///
73/// This structure holds the final result value, any output generated,
74/// parsing errors encountered, and the tokenized representation of the script.
75/// It's particularly useful for debugging and deobfuscation purposes.
76///
77/// # Examples
78///
79/// ```rust
80/// use ps_parser::PowerShellSession;
81///
82/// let mut session = PowerShellSession::new();
83/// let script_result = session.parse_input("$a = 42; $a").unwrap();
84///
85/// // Access different parts of the result
86/// println!("Final value: {:?}", script_result.result());
87/// println!("Output: {:?}", script_result.output());
88/// println!("Errors: {:?}", script_result.errors());
89/// ```
90///
91/// Represents a parsed token from a PowerShell script.
92///
93/// Tokens are the building blocks of parsed PowerShell code and are used
94/// for syntax analysis, deobfuscation, and code transformation.
95///
96/// # Examples
97///
98/// ```rust
99/// use ps_parser::PowerShellSession;
100///
101/// let mut session = PowerShellSession::new();
102/// let script_result = session.parse_input("$var = 123").unwrap();
103///
104/// // Inspect the tokens
105/// for token in script_result.tokens().all() {
106///     println!("Token: {:?}", token);
107/// }
108/// ```
109///
110/// Manages PowerShell variables across different scopes.
111///
112/// This structure handles variable storage, retrieval, and scope management
113/// for PowerShell scripts. It supports loading variables from environment
114/// variables, INI files, and manual assignment.
115///
116/// # Examples
117///
118/// ```rust
119/// use ps_parser::{Variables, PowerShellSession};
120/// use std::path::Path;
121///
122/// // Load environment variables
123/// let env_vars = Variables::env();
124/// let mut session = PowerShellSession::new().with_variables(env_vars);
125///
126/// // Load from INI string
127/// let ini_vars = Variables::from_ini_string("[global]\nname = John Doe\n[local]\nlocal_var = \"local_value\"").unwrap();
128/// let mut session2 = PowerShellSession::new().with_variables(ini_vars);
129///
130/// // Create empty and add manually
131/// let mut vars = Variables::new();
132/// // ... add variables manually
133/// ```
134pub use parser::{PowerShellSession, PsValue, ScriptResult, Token, Variables};
135
136#[cfg(test)]
137mod tests {
138    use std::collections::HashMap;
139
140    use super::*;
141    use crate::Token;
142
143    #[test]
144    fn obfuscation_1() {
145        let input = r#"
146$ilryNQSTt="System.$([cHAR]([ByTE]0x4d)+[ChAR]([byte]0x61)+[chAr](110)+[cHar]([byTE]0x61)+[cHaR](103)+[cHar](101*64/64)+[chaR]([byTE]0x6d)+[cHAr](101)+[CHAr]([byTE]0x6e)+[Char](116*103/103)).$([Char]([ByTe]0x41)+[Char](117+70-70)+[CHAr]([ByTE]0x74)+[CHar]([bYte]0x6f)+[CHar]([bytE]0x6d)+[ChaR]([ByTe]0x61)+[CHar]([bYte]0x74)+[CHAR]([byte]0x69)+[Char](111*26/26)+[chAr]([BYTe]0x6e)).$(('Ârmí'+'Ùtìl'+'s').NORmalizE([ChAR](44+26)+[chAR](111*9/9)+[cHar](82+32)+[ChaR](109*34/34)+[cHaR](68+24-24)) -replace [ChAr](92)+[CHaR]([BYTe]0x70)+[Char]([BytE]0x7b)+[CHaR]([BYTe]0x4d)+[chAR](110)+[ChAr](15+110))";$ilryNQSTt
147"#;
148
149        let mut p = PowerShellSession::new();
150        assert_eq!(
151            p.safe_eval(input).unwrap().as_str(),
152            "System.Management.Automation.ArmiUtils"
153        );
154    }
155
156    #[test]
157    fn obfuscation_2() {
158        let input = r#"
159$(('W'+'r'+'î'+'t'+'é'+'Í'+'n'+'t'+'3'+'2').NormAlIzE([chaR]([bYTE]0x46)+[CHAR](111)+[ChAR]([Byte]0x72)+[CHAR]([BytE]0x6d)+[CHAr](64+4)) -replace [cHAr]([BytE]0x5c)+[char]([bYtE]0x70)+[ChAR]([byTe]0x7b)+[cHar]([bYtE]0x4d)+[Char]([bYte]0x6e)+[CHAR](125))
160"#;
161
162        let mut p = PowerShellSession::new();
163        assert_eq!(p.safe_eval(input).unwrap().as_str(), "WriteInt32");
164    }
165
166    #[test]
167    fn obfuscation_3() {
168        let input = r#"
169$([cHar]([BYte]0x65)+[chAr]([bYTE]0x6d)+[CHaR]([ByTe]0x73)+[char](105)+[CHAR]([bYTE]0x43)+[cHaR](111)+[chaR]([bYTE]0x6e)+[cHAr]([bYTe]0x74)+[cHAr](32+69)+[cHaR](120+30-30)+[cHAR]([bYte]0x74))
170"#;
171
172        let mut p = PowerShellSession::new();
173        assert_eq!(p.safe_eval(input).unwrap().as_str(), "emsiContext");
174    }
175
176    #[test]
177    fn obfuscation_4() {
178        let input = r#"
179[syStem.texT.EncoDInG]::unIcoDe.geTstRiNg([SYSTem.cOnVERT]::froMbasE64striNg("WwBjAGgAYQByAF0AKABbAGkAbgB0AF0AKAAiADkAZQA0AGUAIgAgAC0AcgBlAHAAbABhAGMAZQAgACIAZQAiACkAKwAzACkA"))"#;
180
181        let mut p = PowerShellSession::new();
182        assert_eq!(
183            p.safe_eval(input).unwrap().as_str(),
184            r#"[char]([int]("9e4e" -replace "e")+3)"#
185        );
186    }
187
188    #[test]
189    fn deobfuscation() {
190        // assign variable and print it to screen
191        let mut p = PowerShellSession::new();
192        let input = r#" $global:var = [char]([int]("9e4e" -replace "e")+3); [int]'a';$var"#;
193        let script_res = p.parse_input(input).unwrap();
194        assert_eq!(script_res.result(), 'a'.into());
195        assert_eq!(
196            script_res.deobfuscated(),
197            vec!["$var = 'a'", "[int]'a'"].join(NEWLINE)
198        );
199        assert_eq!(script_res.errors().len(), 1);
200        assert_eq!(
201            script_res.errors()[0].to_string(),
202            "ValError: Cannot convert value \"String\" to type \"Int\""
203        );
204
205        // the same but do it in two parts
206        let mut p = PowerShellSession::new();
207        let input = r#" $global:var = [char]([int]("9e4e" -replace "e")+3) "#;
208        let script_res = p.parse_input(input).unwrap();
209
210        assert_eq!(script_res.errors().len(), 0);
211
212        let script_res = p.parse_input(" [int]'a';$var ").unwrap();
213        assert_eq!(script_res.deobfuscated(), vec!["[int]'a'"].join(NEWLINE));
214        assert_eq!(script_res.output(), vec!["a"].join(NEWLINE));
215        assert_eq!(script_res.errors().len(), 1);
216        assert_eq!(
217            script_res.errors()[0].to_string(),
218            "ValError: Cannot convert value \"String\" to type \"Int\""
219        );
220    }
221
222    #[test]
223    fn deobfuscation_non_existing_value() {
224        // assign not existing value, without forcing evaluation
225        let mut p = PowerShellSession::new();
226        let input = r#" $local:var = $env:programfiles;[int]'a';$var"#;
227        let script_res = p.parse_input(input).unwrap();
228        assert_eq!(script_res.result(), PsValue::Null);
229        assert_eq!(
230            script_res.deobfuscated(),
231            vec!["$local:var = $env:programfiles", "[int]'a'", "$var"].join(NEWLINE)
232        );
233        assert_eq!(script_res.errors().len(), 3);
234        assert_eq!(
235            script_res.errors()[0].to_string(),
236            "VariableError: Variable \"programfiles\" is not defined"
237        );
238        assert_eq!(
239            script_res.errors()[1].to_string(),
240            "ValError: Cannot convert value \"String\" to type \"Int\""
241        );
242        assert_eq!(
243            script_res.errors()[2].to_string(),
244            "VariableError: Variable \"var\" is not defined"
245        );
246
247        // assign not existing value, forcing evaluation
248        let mut p = PowerShellSession::new().with_variables(Variables::force_eval());
249        let input = r#" $global:var = $env:programfiles;[int]'a';$var"#;
250        let script_res = p.parse_input(input).unwrap();
251        assert_eq!(script_res.result(), PsValue::Null);
252        assert_eq!(
253            script_res.deobfuscated(),
254            vec!["$var = $null", "[int]'a'"].join(NEWLINE)
255        );
256        assert_eq!(script_res.errors().len(), 1);
257    }
258
259    #[test]
260    fn deobfuscation_env_value() {
261        // assign not existing value, without forcing evaluation
262        let mut p = PowerShellSession::new().with_variables(Variables::env());
263        let input = r#" $global:var = $env:programfiles;$var"#;
264        let script_res = p.parse_input(input).unwrap();
265        assert_eq!(
266            script_res.result(),
267            PsValue::String(std::env::var("PROGRAMFILES").unwrap())
268        );
269        assert_eq!(
270            script_res.deobfuscated(),
271            vec![format!(
272                "$var = '{}'",
273                std::env::var("PROGRAMFILES").unwrap()
274            )]
275            .join(NEWLINE)
276        );
277        assert_eq!(script_res.errors().len(), 0);
278    }
279
280    #[test]
281    fn hash_table() {
282        // assign not existing value, without forcing evaluation
283        let mut p = PowerShellSession::new().with_variables(Variables::env());
284        let input = r#" 
285$nestedData = @{
286    Users = @(
287        @{ Name = "Alice"; Age = 30; Skills = @("PowerShell", "Python") }
288        @{ Name = "Bob"; Age = 25; Skills = @("Java", "C#") }
289    )
290    Settings = @{
291        Theme = "Dark"
292        Language = "en-US"
293    }
294}
295"$nestedData"
296        "#;
297        let script_res = p.parse_input(input).unwrap();
298        assert_eq!(
299            script_res.result(),
300            PsValue::String("System.Collections.Hashtable".to_string())
301        );
302
303        assert_eq!(
304            p.parse_input("$nesteddata.settings").unwrap().result(),
305            PsValue::HashTable(HashMap::from([
306                ("language".to_string(), PsValue::String("en-US".to_string())),
307                ("theme".to_string(), PsValue::String("Dark".to_string())),
308            ]))
309        );
310
311        assert_eq!(
312            p.safe_eval("$nesteddata.settings.theme").unwrap(),
313            "Dark".to_string()
314        );
315
316        assert_eq!(
317            p.parse_input("$nesteddata.users[0]").unwrap().result(),
318            PsValue::HashTable(HashMap::from([
319                (
320                    "skills".to_string(),
321                    PsValue::Array(vec![
322                        PsValue::String("PowerShell".to_string()),
323                        PsValue::String("Python".to_string().into())
324                    ])
325                ),
326                ("name".to_string(), PsValue::String("Alice".to_string())),
327                ("age".to_string(), PsValue::Int(30)),
328            ]))
329        );
330
331        assert_eq!(
332            p.safe_eval("$nesteddata.users[0]['name']").unwrap(),
333            "Alice".to_string()
334        );
335
336        assert_eq!(
337            p.safe_eval("$nesteddata.users[0].NAME").unwrap(),
338            "Alice".to_string()
339        );
340    }
341
342    #[test]
343    fn test_simple_arithmetic() {
344        let input = r#"
345Write-Host "=== Test 3: Arithmetic Operations ===" -ForegroundColor Green
346$a = 10
347$b = 5
348Write-Output "Addition: $(($a + $b))"
349Write-Output "Subtraction: $(($a - $b))"
350Write-Output "Multiplication: $(($a * $b))"
351Write-Output "Division: $(($a / $b))"
352Write-Output "Modulo: $(($a % $b))"
353"#;
354
355        let script_result = PowerShellSession::new().parse_input(input).unwrap();
356
357        assert_eq!(script_result.result(), PsValue::String("Modulo: 0".into()));
358        assert_eq!(
359            script_result.output(),
360            vec![
361                r#"=== Test 3: Arithmetic Operations ==="#,
362                r#"Addition: 15"#,
363                r#"Subtraction: 5"#,
364                r#"Multiplication: 50"#,
365                r#"Division: 2"#,
366                r#"Modulo: 0"#
367            ]
368            .join(NEWLINE)
369        );
370        assert_eq!(script_result.errors().len(), 0);
371        assert_eq!(script_result.tokens().strings(), vec![]);
372        assert_eq!(script_result.tokens().expandable_strings().len(), 6);
373        assert_eq!(
374            script_result.tokens().expandable_strings()[1],
375            Token::StringExpandable(
376                "\"Addition: $(($a + $b))\"".to_string(),
377                "Addition: 15".to_string()
378            )
379        );
380        assert_eq!(script_result.tokens().expression().len(), 12);
381        assert_eq!(
382            script_result.tokens().expression()[2],
383            Token::Expression("$a + $b".to_string(), PsValue::Int(15))
384        );
385    }
386
387    #[test]
388    fn test_scripts() {
389        use std::fs;
390        let Ok(entries) = fs::read_dir("test_scripts") else {
391            panic!("Failed to read test files");
392        };
393        for entry in entries {
394            let dir_entry = entry.unwrap();
395            if std::fs::FileType::is_dir(&dir_entry.file_type().unwrap()) {
396                // If it's a directory, we can read the files inside it
397                let input_script = dir_entry.path().join("input.ps1");
398                let deobfuscated = dir_entry.path().join("deobfuscated.txt");
399                let output = dir_entry.path().join("output.txt");
400
401                let Ok(content) = fs::read_to_string(&input_script) else {
402                    panic!("Failed to read test files");
403                };
404
405                let Ok(deobfuscated) = fs::read_to_string(&deobfuscated) else {
406                    panic!("Failed to read test files");
407                };
408
409                let Ok(output) = fs::read_to_string(&output) else {
410                    panic!("Failed to read test files");
411                };
412
413                let script_result = PowerShellSession::new()
414                    .with_variables(Variables::env())
415                    .parse_input(&content)
416                    .unwrap();
417
418                let deobfuscated_vec = deobfuscated
419                    .lines()
420                    .map(|s| s.trim_end())
421                    .collect::<Vec<&str>>();
422
423                let script_deobfuscated = script_result.deobfuscated();
424
425                let output_vec = output.lines().map(|s| s.trim_end()).collect::<Vec<&str>>();
426
427                let script_output = script_result.output();
428
429                let _name = dir_entry
430                    .path()
431                    .components()
432                    .last()
433                    .unwrap()
434                    .as_os_str()
435                    .to_string_lossy()
436                    .to_string();
437                // std::fs::write(
438                //     format!("{}_deobfuscated.txt", _name),
439                //     script_deobfuscated.clone(),
440                // )
441                // .unwrap();
442                // std::fs::write(format!("{}_output.txt", _name),
443                // script_output.clone()).unwrap();
444                let script_deobfuscated_vec = script_deobfuscated
445                    .lines()
446                    .map(|s| s.trim_end())
447                    .collect::<Vec<&str>>();
448
449                let script_output_vec = script_output
450                    .lines()
451                    .map(|s| s.trim_end())
452                    .collect::<Vec<&str>>();
453
454                for i in 0..deobfuscated_vec.len() {
455                    assert_eq!(deobfuscated_vec[i], script_deobfuscated_vec[i]);
456                }
457
458                for i in 0..output_vec.len() {
459                    assert_eq!(output_vec[i], script_output_vec[i]);
460                }
461            }
462        }
463    }
464
465    #[test]
466    fn test_range() {
467        // Test for even numbers
468        let mut p = PowerShellSession::new().with_variables(Variables::env());
469        let input = r#" $numbers = 1..10; $numbers"#;
470        let script_res = p.parse_input(input).unwrap();
471        assert_eq!(
472            script_res.deobfuscated(),
473            vec!["$numbers = @(1,2,3,4,5,6,7,8,9,10)"].join(NEWLINE)
474        );
475        assert_eq!(script_res.errors().len(), 0);
476    }
477
478    #[test]
479    fn even_numbers() {
480        // Test for even numbers
481        let mut p = PowerShellSession::new().with_variables(Variables::env());
482        let input = r#" $numbers = 1..10; $evenNumbers = $numbers | Where-Object { $_ % 2 -eq 0 }; $evenNumbers"#;
483        let script_res = p.parse_input(input).unwrap();
484        assert_eq!(
485            script_res.result(),
486            PsValue::Array(vec![
487                PsValue::Int(2),
488                PsValue::Int(4),
489                PsValue::Int(6),
490                PsValue::Int(8),
491                PsValue::Int(10)
492            ])
493        );
494        assert_eq!(
495            script_res.deobfuscated(),
496            vec![
497                "$numbers = @(1,2,3,4,5,6,7,8,9,10)",
498                "$evennumbers = @(2,4,6,8,10)"
499            ]
500            .join(NEWLINE)
501        );
502        assert_eq!(script_res.errors().len(), 0);
503    }
504
505    //#[test]
506    fn _test_function() {
507        // Test for even numbers
508        let mut p = PowerShellSession::new().with_variables(Variables::env());
509        let input = r#" 
510function Get-Square($number) {
511    return $number * $number
512}
513"Square of 5: $(Get-Square 5)" "#;
514        let script_res = p.parse_input(input).unwrap();
515        assert_eq!(
516            script_res.deobfuscated(),
517            vec![
518                "function Get-Square($number) {",
519                "    return $number * $number",
520                "}",
521                " \"Square of 5: $(Get-Square 5)\""
522            ]
523            .join(NEWLINE)
524        );
525        assert_eq!(script_res.errors().len(), 2);
526    }
527}