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