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