1mod parser;
30pub(crate) use parser::NEWLINE;
31pub use parser::PowerShellSession;
54pub use parser::PsValue;
70pub use parser::ScriptResult;
90pub use parser::Token;
122pub 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 deobfuscation() {
157 let mut p = PowerShellSession::new();
159 let input = r#" $script:var = [char]([int]("9e4e" -replace "e")+3); [int]'a';$var"#;
160 let script_res = p.parse_input(input).unwrap();
161 assert_eq!(script_res.result(), 'a'.into());
162 assert_eq!(
163 script_res.deobfuscated(),
164 vec!["$script:var = 'a'", "[int]'a'", "'a'"].join(NEWLINE)
165 );
166 assert_eq!(script_res.errors().len(), 1);
167 assert_eq!(
168 script_res.errors()[0].to_string(),
169 "ValError: Cannot convert value \"String\" to type \"Int\""
170 );
171
172 let mut p = PowerShellSession::new();
174 let input = r#" $global:var = [char]([int]("9e4e" -replace "e")+3) "#;
175 let script_res = p.parse_input(input).unwrap();
176
177 assert_eq!(script_res.errors().len(), 0);
178
179 let script_res = p.parse_input(" [int]'a';$var ").unwrap();
180 assert_eq!(
181 script_res.deobfuscated(),
182 vec!["[int]'a'", "'a'"].join(NEWLINE)
183 );
184 assert_eq!(script_res.output(), vec!["a"].join(NEWLINE));
185 assert_eq!(script_res.errors().len(), 1);
186 assert_eq!(
187 script_res.errors()[0].to_string(),
188 "ValError: Cannot convert value \"String\" to type \"Int\""
189 );
190 }
191
192 #[test]
193 fn deobfuscation_non_existing_value() {
194 let mut p = PowerShellSession::new();
196 let input = r#" $local:var = $env:programfiles;[int]'a';$var"#;
197 let script_res = p.parse_input(input).unwrap();
198 assert_eq!(script_res.result(), PsValue::Null);
199 assert_eq!(
200 script_res.deobfuscated(),
201 vec!["$local:var = $env:programfiles", "[int]'a'", "$var"].join(NEWLINE)
202 );
203 assert_eq!(script_res.errors().len(), 3);
204 assert_eq!(
205 script_res.errors()[0].to_string(),
206 "VariableError: Variable \"programfiles\" is not defined"
207 );
208 assert_eq!(
209 script_res.errors()[1].to_string(),
210 "ValError: Cannot convert value \"String\" to type \"Int\""
211 );
212 assert_eq!(
213 script_res.errors()[2].to_string(),
214 "VariableError: Variable \"var\" is not defined"
215 );
216
217 let mut p = PowerShellSession::new().with_variables(Variables::force_eval());
219 let input = r#" $local:var = $env:programfiles;[int]'a';$script:var"#;
220 let script_res = p.parse_input(input).unwrap();
221 assert_eq!(script_res.result(), PsValue::Null);
222 assert_eq!(
223 script_res.deobfuscated(),
224 vec!["$local:var = $null", "[int]'a'"].join(NEWLINE)
225 );
226 assert_eq!(script_res.errors().len(), 1);
227 }
228
229 #[test]
230 fn deobfuscation_env_value() {
231 let mut p = PowerShellSession::new().with_variables(Variables::env());
233 let input = r#" $local:var = $env:programfiles;$var"#;
234 let script_res = p.parse_input(input).unwrap();
235 assert_eq!(
236 script_res.result(),
237 PsValue::String(std::env::var("PROGRAMFILES").unwrap())
238 );
239 let program_files = std::env::var("PROGRAMFILES").unwrap();
240 assert_eq!(
241 script_res.deobfuscated(),
242 vec![
243 format!("$local:var = \"{}\"", program_files),
244 format!("\"{}\"", program_files)
245 ]
246 .join(NEWLINE)
247 );
248 assert_eq!(script_res.errors().len(), 0);
249 }
250
251 #[test]
252 fn hash_table() {
253 let mut p = PowerShellSession::new().with_variables(Variables::env().values_persist());
255 let input = r#"
256$nestedData = @{
257 Users = @(
258 @{ Name = "Alice"; Age = 30; Skills = @("PowerShell", "Python") }
259 @{ Name = "Bob"; Age = 25; Skills = @("Java", "C#") }
260 )
261 Settings = @{
262 Theme = "Dark"
263 Language = "en-US"
264 }
265}
266"$nestedData"
267 "#;
268 let script_res = p.parse_input(input).unwrap();
269 assert_eq!(
270 script_res.result(),
271 PsValue::String("System.Collections.Hashtable".to_string())
272 );
273
274 assert_eq!(
275 p.parse_input("$nesteddata.settings").unwrap().result(),
276 PsValue::HashTable(HashMap::from([
277 ("language".to_string(), PsValue::String("en-US".to_string())),
278 ("theme".to_string(), PsValue::String("Dark".to_string())),
279 ]))
280 );
281
282 assert_eq!(
283 p.safe_eval("$nesteddata.settings.theme").unwrap(),
284 "Dark".to_string()
285 );
286
287 assert_eq!(
288 p.parse_input("$nesteddata.users[0]").unwrap().result(),
289 PsValue::HashTable(HashMap::from([
290 (
291 "skills".to_string(),
292 PsValue::Array(vec![
293 PsValue::String("PowerShell".to_string()),
294 PsValue::String("Python".to_string().into())
295 ])
296 ),
297 ("name".to_string(), PsValue::String("Alice".to_string())),
298 ("age".to_string(), PsValue::Int(30)),
299 ]))
300 );
301
302 assert_eq!(
303 p.safe_eval("$nesteddata.users[0]['name']").unwrap(),
304 "Alice".to_string()
305 );
306
307 assert_eq!(
308 p.safe_eval("$nesteddata.users[0].NAME").unwrap(),
309 "Alice".to_string()
310 );
311
312 let input = r#" $a=@{val = 4};$a.val"#;
313 let script_res = p.parse_input(input).unwrap();
314 assert_eq!(script_res.result(), PsValue::Int(4));
315 assert_eq!(
316 script_res.deobfuscated(),
317 vec!["$a = @{", "\tval = 4", "}", "4"].join(NEWLINE)
318 );
319 }
320
321 #[test]
322 fn test_simple_arithmetic() {
323 let input = r#"
324Write-Host "=== Test 3: Arithmetic Operations ===" -ForegroundColor Green
325$a = 10
326$b = 5
327Write-Output "Addition: $(($a + $b))"
328Write-Output "Subtraction: $(($a - $b))"
329Write-Output "Multiplication: $(($a * $b))"
330Write-Output "Division: $(($a / $b))"
331Write-Output "Modulo: $(($a % $b))"
332"#;
333
334 let script_result = PowerShellSession::new().parse_input(input).unwrap();
335
336 assert_eq!(script_result.result(), PsValue::String("Modulo: 0".into()));
337 assert_eq!(
338 script_result.output(),
339 vec![
340 r#"=== Test 3: Arithmetic Operations ==="#,
341 r#"Addition: 15"#,
342 r#"Subtraction: 5"#,
343 r#"Multiplication: 50"#,
344 r#"Division: 2"#,
345 r#"Modulo: 0"#
346 ]
347 .join(NEWLINE)
348 );
349 assert_eq!(script_result.errors().len(), 0);
350 assert_eq!(script_result.tokens().expandable_strings().len(), 6);
351 assert_eq!(
352 script_result.tokens().expandable_strings()[1],
353 Token::StringExpandable(
354 "\"Addition: $(($a + $b))\"".to_string(),
355 "Addition: 15".to_string()
356 )
357 );
358 assert_eq!(script_result.tokens().expression().len(), 12);
359 assert_eq!(
360 script_result.tokens().expression()[2],
361 Token::Expression("$a + $b".to_string(), PsValue::Int(15))
362 );
363 }
364
365 #[test]
366 fn test_scripts() {
367 use std::fs;
368 let Ok(entries) = fs::read_dir("test_scripts") else {
369 panic!("Failed to read 'test_scripts' directory");
370 };
371 for entry in entries {
372 let dir_entry = entry.unwrap();
373 if std::fs::FileType::is_dir(&dir_entry.file_type().unwrap()) {
374 let input_script = dir_entry.path().join("input.ps1");
376 let expected_deobfuscated_script = dir_entry.path().join("deobfuscated.txt");
377 let expected_output_script = dir_entry.path().join("output.txt");
378
379 let Ok(input) = fs::read_to_string(&input_script) else {
380 panic!("Failed to read test file: {}", input_script.display());
381 };
382
383 let Ok(expected_deobfuscated) = fs::read_to_string(&expected_deobfuscated_script)
384 else {
385 panic!(
386 "Failed to read test file: {}",
387 expected_deobfuscated_script.display()
388 );
389 };
390
391 let Ok(expected_output) = fs::read_to_string(&expected_output_script) else {
392 panic!(
393 "Failed to read test file: {}",
394 expected_output_script.display()
395 );
396 };
397
398 let script_result = PowerShellSession::new()
399 .with_variables(Variables::env())
400 .parse_input(&input)
401 .unwrap();
402
403 let expected_deobfuscated_vec = expected_deobfuscated
404 .lines()
405 .map(|s| s.trim_end())
406 .collect::<Vec<&str>>();
407
408 let current_deobfuscated = script_result.deobfuscated();
409 let current_output = script_result.output();
410
411 let expected_output_vec = expected_output
412 .lines()
413 .map(|s| s.trim_end())
414 .collect::<Vec<&str>>();
415
416 let current_deobfuscated_vec = current_deobfuscated
418 .lines()
419 .map(|s| s.trim_end())
420 .collect::<Vec<&str>>();
421
422 let current_output_vec = current_output
423 .lines()
424 .map(|s| s.trim_end())
425 .collect::<Vec<&str>>();
426
427 for i in 0..expected_deobfuscated_vec.len() {
428 assert_eq!(
429 expected_deobfuscated_vec[i],
430 current_deobfuscated_vec[i],
431 "File: {}, Deobfuscated line: {}",
432 file_name(&dir_entry),
433 i + 1
434 );
435 }
436
437 for i in 0..expected_output_vec.len() {
438 assert_eq!(
439 expected_output_vec[i],
440 current_output_vec[i],
441 "File: {}, Output line: {}",
442 file_name(&dir_entry),
443 i + 1
444 );
445 }
446 }
447 }
448 }
449
450 fn file_name(dir_entry: &std::fs::DirEntry) -> String {
451 dir_entry
452 .path()
453 .components()
454 .last()
455 .unwrap()
456 .as_os_str()
457 .to_string_lossy()
458 .to_string()
459 }
460
461 #[allow(dead_code)]
462 fn save_files(dir_entry: &std::fs::DirEntry, deobfuscated: &str, output: &str) {
463 let name = file_name(dir_entry);
464 std::fs::write(format!("{}_deobfuscated.txt", name), deobfuscated).unwrap();
465 std::fs::write(format!("{}_output.txt", name), output).unwrap();
466 }
467
468 #[test]
469 fn test_range() {
470 let mut p = PowerShellSession::new().with_variables(Variables::env());
472 let input = r#" $numbers = 1..10; $numbers"#;
473 let script_res = p.parse_input(input).unwrap();
474 assert_eq!(
475 script_res.deobfuscated(),
476 vec![
477 "$numbers = @(1,2,3,4,5,6,7,8,9,10)",
478 "@(1,2,3,4,5,6,7,8,9,10)"
479 ]
480 .join(NEWLINE)
481 );
482 assert_eq!(script_res.errors().len(), 0);
483 }
484
485 #[test]
486 fn even_numbers() {
487 let mut p = PowerShellSession::new().with_variables(Variables::env());
489 let input = r#" $numbers = 1..10; $evenNumbers = $numbers | Where-Object { $_ % 2 -eq 0 }; $evenNumbers"#;
490 let script_res = p.parse_input(input).unwrap();
491 assert_eq!(
492 script_res.result(),
493 PsValue::Array(vec![
494 PsValue::Int(2),
495 PsValue::Int(4),
496 PsValue::Int(6),
497 PsValue::Int(8),
498 PsValue::Int(10)
499 ])
500 );
501 assert_eq!(
502 script_res.deobfuscated(),
503 vec![
504 "$numbers = @(1,2,3,4,5,6,7,8,9,10)",
505 "$evennumbers = @(2,4,6,8,10)",
506 "@(2,4,6,8,10)"
507 ]
508 .join(NEWLINE)
509 );
510 assert_eq!(script_res.errors().len(), 0);
511 }
512
513 #[test]
514 fn divisible_by_2_and_3() {
515 let mut p = PowerShellSession::new().with_variables(Variables::env());
517 let input = r#" $numbers = 1..10; $numbers | Where { $_ % 2 -eq 0 } | ? { $_ % 3 -eq 0 }"#;
518 let script_res = p.parse_input(input).unwrap();
519 assert_eq!(script_res.result(), PsValue::Int(6));
520 assert_eq!(
521 script_res.deobfuscated(),
522 vec!["$numbers = @(1,2,3,4,5,6,7,8,9,10)", "6"].join(NEWLINE)
523 );
524 assert_eq!(script_res.errors().len(), 0);
525 }
526
527 fn _test_function() {
529 let mut p = PowerShellSession::new().with_variables(Variables::env());
531 let input = r#"
532function Get-Square($number) {
533 return $number * $number
534}
535"Square of 5: $(Get-Square 5)" "#;
536 let script_res = p.parse_input(input).unwrap();
537 assert_eq!(
538 script_res.deobfuscated(),
539 vec![
540 "function Get-Square($number) {",
541 " return $number * $number",
542 "}",
543 " \"Square of 5: $(Get-Square 5)\""
544 ]
545 .join(NEWLINE)
546 );
547 assert_eq!(script_res.errors().len(), 2);
548 }
549
550 #[test]
551 fn test_if() {
552 let mut p = PowerShellSession::new().with_variables(Variables::env());
554 let input = r#"
555 # Test 10: Conditional Statements
556if ($true) {
557 $if_result = "condition true"
558}
559
560if ($false) {
561 $else_result = "false branch"
562} else {
563 $else_result = "true branch"
564}
565
566$score = 85
567if ($score -ge 90) {
568 $grade = "A"
569} elseif ($score -ge 80) {
570 $grade = "B"
571} else {
572 $grade = "C"
573}
574
575 "#;
576 let script_res = p.parse_input(input).unwrap();
577 assert_eq!(
578 script_res.deobfuscated(),
579 vec![
580 "$if_result = \"condition true\"",
581 "$else_result = \"true branch\"",
582 "$score = 85",
583 "$grade = \"B\""
584 ]
585 .join(NEWLINE)
586 );
587 assert_eq!(script_res.errors().len(), 0);
588 }
589
590 #[test]
591 fn format_operator() {
592 let mut p = PowerShellSession::new().with_variables(Variables::env());
593 let input = r#" ("{5}{2}{0}{1}{3}{6}{4}" -f 'ut',('oma'+'t'+'ion.'),'.A',('Ems'+'iUt'),'ls',('S'+'ystem.'+'Danage'+'men'+'t'),'i')"#;
594 let script_res = p.parse_input(input).unwrap();
595 assert_eq!(
596 script_res.result(),
597 PsValue::String("System.Danagement.Automation.EmsiUtils".into())
598 );
599 assert_eq!(
600 script_res.deobfuscated(),
601 vec![r#""System.Danagement.Automation.EmsiUtils""#].join(NEWLINE)
602 );
603 assert_eq!(script_res.errors().len(), 0);
604 }
605
606 #[test]
607 fn encod_command() {
608 let mut p = PowerShellSession::new().with_variables(Variables::env());
609 let input = r#" ("{5}{2}{0}{1}{3}{6}{4}" -f 'ut',('oma'+'t'+'ion.'),'.A',('Ems'+'iUt'),'ls',('S'+'ystem.'+'Danage'+'men'+'t'),'i')"#;
610 let script_res = p.parse_input(input).unwrap();
611 assert_eq!(
612 script_res.result(),
613 PsValue::String("System.Danagement.Automation.EmsiUtils".into())
614 );
615 assert_eq!(
616 script_res.deobfuscated(),
617 vec![r#""System.Danagement.Automation.EmsiUtils""#].join(NEWLINE)
618 );
619 assert_eq!(script_res.errors().len(), 0);
620 }
621
622 #[test]
623 fn array_literals() {
624 let mut p = PowerShellSession::new().with_variables(Variables::env());
625
626 let input = r#" $a = 1,2,3;$a"#;
628 let script_res = p.parse_input(input).unwrap();
629 assert_eq!(
630 script_res.result(),
631 PsValue::Array(vec![PsValue::Int(1), PsValue::Int(2), PsValue::Int(3)])
632 );
633 assert_eq!(
634 script_res.deobfuscated(),
635 vec!["$a = @(1,2,3)", "@(1,2,3)"].join(NEWLINE)
636 );
637
638 let input = r#" $a = "x", 'yyy', "z";$a"#;
640 let script_res = p.parse_input(input).unwrap();
641 assert_eq!(
642 script_res.result(),
643 PsValue::Array(vec![
644 PsValue::String("x".into()),
645 PsValue::String("yyy".into()),
646 PsValue::String("z".into())
647 ])
648 );
649 assert_eq!(
650 script_res.deobfuscated(),
651 vec![r#"$a = @("x","yyy","z")"#, r#"@("x","yyy","z")"#].join(NEWLINE)
652 );
653
654 let input = r#" $a = 1,2+ 3,[long]4;$a"#;
656 let script_res = p.parse_input(input).unwrap();
657 assert_eq!(
658 script_res.result(),
659 PsValue::Array(vec![
660 PsValue::Int(1),
661 PsValue::Int(2),
662 PsValue::Int(3),
663 PsValue::Int(4),
664 ])
665 );
666 assert_eq!(
667 script_res.deobfuscated(),
668 vec!["$a = @(1,2,3,4)", "@(1,2,3,4)"].join(NEWLINE)
669 );
670
671 let input = r#" $x = 3; $a = $x, $x+1, "count=$x";$a"#;
673 let script_res = p.parse_input(input).unwrap();
674 assert_eq!(
675 script_res.result(),
676 PsValue::Array(vec![
677 PsValue::Int(3),
678 PsValue::Int(3),
679 PsValue::Int(1),
680 PsValue::String("count=3".into()),
681 ])
682 );
683 assert_eq!(
684 script_res.deobfuscated(),
685 vec![
686 "$x = 3",
687 "$a = @(3,3,1,\"count=3\")",
688 "@(3,3,1,\"count=3\")"
689 ]
690 .join(NEWLINE)
691 );
692
693 let input = r#" $a = (1, 2), (3, 4);$a"#;
695 let script_res = p.parse_input(input).unwrap();
696 assert_eq!(
697 script_res.result(),
698 PsValue::Array(vec![
699 PsValue::Array(vec![PsValue::Int(1), PsValue::Int(2)]),
700 PsValue::Array(vec![PsValue::Int(3), PsValue::Int(4)]),
701 ])
702 );
703 assert_eq!(
704 script_res.deobfuscated(),
705 vec!["$a = @(@(1,2),@(3,4))", "@(@(1,2),@(3,4))"].join(NEWLINE)
706 );
707
708 let input = r#" $a = 1, "two", 3.0, $false, (Get-Date);$a"#;
710 let script_res = p.parse_input(input).unwrap();
711 assert_eq!(
712 script_res.result(),
713 PsValue::Array(vec![
714 PsValue::Int(1),
715 PsValue::String("two".into()),
716 PsValue::Float(3.0),
717 PsValue::Bool(false),
718 PsValue::String("Get-Date".into()),
719 ])
720 );
721 assert_eq!(
722 script_res.deobfuscated(),
723 vec![
724 "$a = @(1,\"two\",3,$false,Get-Date)",
725 "@(1,\"two\",3,$false,Get-Date)"
726 ]
727 .join(NEWLINE)
728 );
729
730 let input = r#" $a = 1, 2,3;$b = $a,4,5;$b"#;
732 let script_res = p.parse_input(input).unwrap();
733 assert_eq!(
734 script_res.result(),
735 PsValue::Array(vec![
736 PsValue::Array(vec![PsValue::Int(1), PsValue::Int(2), PsValue::Int(3)]),
737 PsValue::Int(4),
738 PsValue::Int(5),
739 ])
740 );
741 assert_eq!(
742 script_res.deobfuscated(),
743 vec!["$a = @(1,2,3)", "$b = @(@(1,2,3),4,5)", "@(@(1,2,3),4,5)"].join(NEWLINE)
744 );
745
746 let input = r#" $a = 1,-2,(-3) | ForEach-Object { $_ * 2 };$a"#;
748 let script_res = p.parse_input(input).unwrap();
749 assert_eq!(
750 script_res.result(),
751 PsValue::Array(vec![PsValue::Int(2), PsValue::Int(-4), PsValue::Int(-6),])
752 );
753
754 let input = r#" $a = (1,2,3) | ForEach-Object { $_ };$a"#;
756 let script_res = p.parse_input(input).unwrap();
757 assert_eq!(
758 script_res.result(),
759 PsValue::Array(vec![PsValue::Int(1), PsValue::Int(2), PsValue::Int(3),])
760 );
761 assert_eq!(
762 script_res.deobfuscated(),
763 vec!["$a = @(1,2,3)", "@(1,2,3)"].join(NEWLINE)
764 );
765
766 let input = r#" $a = @{
768 A = 1,2,3
769 B = (4,5),6
770}
771$a"#;
772 let script_res = p.parse_input(input).unwrap();
773 assert_eq!(
774 script_res.result(),
775 PsValue::HashTable(HashMap::from([
776 (
777 "a".into(),
778 PsValue::Array(vec![PsValue::Int(1), PsValue::Int(2), PsValue::Int(3),])
779 ),
780 (
781 "b".into(),
782 PsValue::Array(vec![
783 PsValue::Array(vec![PsValue::Int(4), PsValue::Int(5)]),
784 PsValue::Int(6),
785 ])
786 ),
787 ]))
788 );
789
790 let input = r#" function Foo($x) { $x.GetType().name + $x[2]};Foo(1,2,3)"#;
792 let script_res = p.parse_input(input).unwrap();
793 assert_eq!(script_res.result(), PsValue::String("Object[]3".into()));
794
795 let input = r#" $a = ,(42,2);$a"#;
797 let script_res = p.parse_input(input).unwrap();
798 assert_eq!(
799 script_res.result(),
800 PsValue::Array(vec![PsValue::Array(vec![
801 PsValue::Int(42),
802 PsValue::Int(2)
803 ])])
804 );
805
806 let input = r#" function Foo($x) { $x.GetType().name + $x[2]};Foo(1,2,3)"#;
808 let script_res = p.parse_input(input).unwrap();
809 assert_eq!(script_res.result(), PsValue::String("Object[]3".into()));
810
811 let input = r#" function b($x) {$x};b(1,2+3,4)"#;
813 let script_res = p.parse_input(input).unwrap();
814 assert_eq!(
815 script_res.result(),
816 PsValue::Array(vec![
817 PsValue::Int(1),
818 PsValue::Int(2),
819 PsValue::Int(3),
820 PsValue::Int(4),
821 ])
822 );
823
824 let input =
826 r#" $a=@{val = 4};function b($x) {$x};b(1, [long]($a | Where-Object val -eq 4).val)"#;
827 let script_res = p.parse_input(input).unwrap();
828 assert_eq!(
829 script_res.result(),
830 PsValue::Array(vec![PsValue::Int(1), PsValue::Int(4)])
831 );
832 }
833
834 #[test]
835 fn cast_expression() {
836 let mut p = PowerShellSession::new().with_variables(Variables::env());
837
838 let input = r#" $a=@{val = 4};[long]($a).val"#;
840 let script_res = p.parse_input(input).unwrap();
841 assert_eq!(script_res.result(), PsValue::Int(4));
842 assert_eq!(
843 script_res.deobfuscated(),
844 vec!["$a = @{", "\tval = 4", "}", "4"].join(NEWLINE)
845 );
846
847 let input = r#" $a=@{val = 4};[long]($a | Where-Object Val -eq 4).val"#;
848 let script_res = p.parse_input(input).unwrap();
849 assert_eq!(script_res.result(), PsValue::Int(4));
850 assert_eq!(
851 script_res.deobfuscated(),
852 vec!["$a = @{", "\tval = 4", "}", "4"].join(NEWLINE)
853 );
854 }
855}