diff --git a/src/application/Parser/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShell.ts b/src/application/Parser/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShell.ts index 80edbfcd..f7eee436 100644 --- a/src/application/Parser/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShell.ts +++ b/src/application/Parser/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShell.ts @@ -2,11 +2,101 @@ import { IPipe } from '../IPipe'; export class InlinePowerShell implements IPipe { public readonly name: string = 'inlinePowerShell'; - public apply(raw: string): string { - return raw - ?.split(/\r\n|\r|\n/) + public apply(code: string): string { + if (!code || !hasLines(code)) { + return code; + } + code = replaceComments(code); + code = mergeLinesWithBacktick(code); + code = mergeHereStrings(code); + const lines = getLines(code) .map((line) => line.trim()) - .filter((line) => line.length > 0) + .filter((line) => line.length > 0); + return lines .join('; '); } } + +function hasLines(text: string) { + return text.includes('\n') || text.includes('\r'); +} + +/* + Line comments using "#" are replaced with inline comment syntax <# comment.. #> + Otherwise single # comments out rest of the code +*/ +function replaceComments(code: string) { + return code.replaceAll(/#(?])(.*)$/gm, (_$, match1 ) => { + const value = match1?.trim(); + if (!value) { + return '<##>'; + } + return `<# ${value} #>`; + }); +} + +function getLines(code: string) { + return (code.split(/\r\n|\r|\n/) || []); +} + +/* + Merges inline here-strings to a single lined string with Windows line terminator (\r\n) + https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_quoting_rules#here-strings +*/ +function mergeHereStrings(code: string) { + const regex = /@(['"])\s*(?:\r\n|\r|\n)((.|\n|\r)+?)(\r\n|\r|\n)\1@/g; + return code.replaceAll(regex, (_$, quotes, scope) => { + const newString = getHereStringHandler(quotes); + const escaped = scope.replaceAll(quotes, newString.escapedQuotes); + const lines = getLines(escaped); + const inlined = lines.join(newString.separator); + const quoted = `${newString.quotesAround}${inlined}${newString.quotesAround}`; + return quoted; + }); +} +interface IInlinedHereString { + readonly quotesAround: string; + readonly escapedQuotes: string; + readonly separator: string; +} + // We handle @' and @" differently so single quotes are interpreted literally and doubles are expandable +function getHereStringHandler(quotes: string): IInlinedHereString { + const expandableNewLine = '`r`n'; + switch (quotes) { + case '\'': + return { + quotesAround: '\'', + escapedQuotes: '\'\'', + separator: `\'+"${expandableNewLine}"+\'`, + }; + case '"': + return { + quotesAround: '"', + escapedQuotes: '`"', + separator: expandableNewLine, + }; + default: + throw new Error(`expected quotes: ${quotes}`); + } +} + +/* + Input -> + Get-Service * ` + Sort-Object StartType ` + Format-Table Name, ServiceType, Status -AutoSize + Output -> + Get-Service * | Sort-Object StartType | Format-Table -AutoSize +*/ +function mergeLinesWithBacktick(code: string) { + /* + The regex actually wraps any whitespace character after backtick and before newline + However, this is not always the case for PowerShell. + I see two behaviors: + 1. If inside string, it's accepted (inside " or ') + 2. If part of a command, PowerShell throws "An empty pipe element is not allowed" + However we don't need to be so robust and handle this complexity (yet), so for easier regex + we wrap it anyway + */ + return code.replaceAll(/ +`\s*(?:\r\n|\r|\n)\s*/g, ' '); +} diff --git a/src/application/Parser/Script/Syntax/BatchFileSyntax.ts b/src/application/Parser/Script/Syntax/BatchFileSyntax.ts index e10cac7b..59503ca0 100644 --- a/src/application/Parser/Script/Syntax/BatchFileSyntax.ts +++ b/src/application/Parser/Script/Syntax/BatchFileSyntax.ts @@ -1,7 +1,7 @@ import { ILanguageSyntax } from '@/domain/ScriptCode'; -const BatchFileCommonCodeParts = [ '(', ')', 'else' ]; +const BatchFileCommonCodeParts = [ '(', ')', 'else', '||' ]; const PowerShellCommonCodeParts = [ '{', '}' ]; export class BatchFileSyntax implements ILanguageSyntax { diff --git a/src/application/collections/windows.yaml b/src/application/collections/windows.yaml index d5c119b3..8e8c1edb 100644 --- a/src/application/collections/windows.yaml +++ b/src/application/collections/windows.yaml @@ -3659,59 +3659,192 @@ actions: category: Disable OS components for Defender # Hackers way of disabling Defender children: - - category: Disable Windows Defender tasks + category: Disable Defender tasks children: - name: Disable Windows Defender ExploitGuard task + docs: https://www.microsoft.com/security/blog/2017/10/23/windows-defender-exploit-guard-reduce-the-attack-surface-against-next-generation-malware/ code: schtasks /Change /TN "Microsoft\Windows\ExploitGuard\ExploitGuard MDM policy Refresh" /Disable revertCode: schtasks /Change /TN "Microsoft\Windows\ExploitGuard\ExploitGuard MDM policy Refresh" /Enable - name: Disable Windows Defender Cache Maintenance task + # Cache Maintenance is the storage for temporary files that are being either quarantined by Windows Defender + # or being checked. Running this will clear the Cache. + docs: https://answers.microsoft.com/en-us/windows/forum/all/win10-windows-defender-schedulable-tasks-what-does/968ddd6b-3a71-46ce-bc80-d2af11f7e1ae code: schtasks /Change /TN "Microsoft\Windows\Windows Defender\Windows Defender Cache Maintenance" /Disable revertCode: schtasks /Change /TN "Microsoft\Windows\Windows Defender\Windows Defender Cache Maintenance" /Enable - name: Disable Windows Defender Cleanup task + docs: https://answers.microsoft.com/en-us/windows/forum/all/win10-windows-defender-schedulable-tasks-what-does/968ddd6b-3a71-46ce-bc80-d2af11f7e1ae + # Periodic cleanup task + # Clears up files that are not needed anymore by Windows Defender. code: schtasks /Change /TN "Microsoft\Windows\Windows Defender\Windows Defender Cleanup" /Disable revertCode: schtasks /Change /TN "Microsoft\Windows\Windows Defender\Windows Defender Cleanup" /Enable - - name: Disable Windows Defender Scheduled Scan task + name: Disable Windows Defender Scheduled Scan task # May not exist + docs: + - https://support.microsoft.com/en-us/windows/schedule-a-scan-in-microsoft-defender-antivirus-54b64e9c-880a-c6b6-2416-0eb330ed5d2d + - https://winbuzzer.com/2020/05/26/windows-defender-how-to-perform-a-scheduled-scan-in-windows-10-xcxwbt/ code: schtasks /Change /TN "Microsoft\Windows\Windows Defender\Windows Defender Scheduled Scan" /Disable revertCode: schtasks /Change /TN "Microsoft\Windows\Windows Defender\Windows Defender Scheduled Scan" /Enable - name: Disable Windows Defender Verification task + # Check if there are any problems with your Windows Defender like in updates, system files, etc,. + # Creates daily restore points + docs: + - https://answers.microsoft.com/en-us/windows/forum/all/win10-windows-defender-schedulable-tasks-what-does/968ddd6b-3a71-46ce-bc80-d2af11f7e1ae + - https://answers.microsoft.com/en-us/windows/forum/all/windows-defender-system-restore-points/86f77a7f-4ee9-411f-b016-223993c55426 + - https://www.windowsphoneinfo.com/threads/same-problems-with-windows-defender-verification-and-scan-tasks.121489/#Same_problems_with_Windows_Defender_Verification_and_Scan_Tasks code: schtasks /Change /TN "Microsoft\Windows\Windows Defender\Windows Defender Verification" /Disable revertCode: schtasks /Change /TN "Microsoft\Windows\Windows Defender\Windows Defender Verification" /Enable - - category: Disable Windows Defender services + category: Disable Defender services and drivers + # Normally users can disable services on GUI or using commands like "sc config" + # However Defender services are protected with different ways + # 1. Some cannot be disabled (access error) normally but only with DisableServiceInRegistry + # 2. Some cannot be disabled even using DisableServiceInRegistry, must be disabled as TrustedInstaller using RunInlineCodeAsTrustedInstaller children: - name: Disable Windows Defender Firewall service - code: reg add "HKLM\SYSTEM\CurrentControlSet\Services\MpsSvc" /v "Start" /t REG_DWORD /d "4" /f - revertCode: reg add "HKLM\SYSTEM\CurrentControlSet\Services\MpsSvc" /v "Start" /t REG_DWORD /d "2" /f + docs: http://batcmd.com/windows/10/services/mpssvc/ + call: + - + function: DisableServiceInRegistry # We must disable it on registry level, "Access is denied" for sc config + parameters: + serviceName: MpsSvc + defaultStartUpMode: 2 # 0: Boot | 1: System | 2: Automatic | 3: Manual | 4: Disabled + - + function: RenameSystemFile + parameters: + filePath: '%WinDir%\system32\mpssvc.dll' - name: Disable Windows Defender Antivirus service - code: reg add "HKLM\SYSTEM\CurrentControlSet\Services\WinDefend" /v "Start" /t REG_DWORD /d "4" /f - revertCode: reg add "HKLM\SYSTEM\CurrentControlSet\Services\WinDefend" /v "Start" /t REG_DWORD /d "2" /f + docs: http://batcmd.com/windows/10/services/windefend/ + call: + - + function: RunInlineCodeAsTrustedInstaller + parameters: + code: sc stop "WinDefend" & sc config "WinDefend" start=disabled + revertCode: sc config "WinDefend" start=auto & sc start "WinDefend" + # - # "Access is denied" when renaming file + # function: RenameSystemFile + # parameters: + # filePath: '%ProgramFiles%\Windows Defender\MsMpEng.exe' # Found also in C:\ProgramData\Microsoft\Windows Defender\Platform\4.18.2107.4-0 and \4.18.2103.7-0 ... - - name: Disable Microsoft Defender Antivirus Boot Driver service - code: reg add "HKLM\SYSTEM\CurrentControlSet\Services\WdBoot" /v "Start" /t REG_DWORD /d "4" /f - revertCode: reg add "HKLM\SYSTEM\CurrentControlSet\Services\WdBoot" /v "Start" /t REG_DWORD /d "2" /f - - - name: Disable Microsoft Defender Antivirus Mini-Filter Driver service - code: reg add "HKLM\SYSTEM\CurrentControlSet\Services\WdFilter" /v "Start" /t REG_DWORD /d "4" /f - revertCode: reg add "HKLM\SYSTEM\CurrentControlSet\Services\WdFilter" /v "Start" /t REG_DWORD /d "2" /f - - - name: Disable Microsoft Defender Antivirus Network Inspection System Driver service - code: reg add "HKLM\SYSTEM\CurrentControlSet\Services\WdNisDrv" /v "Start" /t REG_DWORD /d "4" /f - revertCode: reg add "HKLM\SYSTEM\CurrentControlSet\Services\WdNisDrv" /v "Start" /t REG_DWORD /d "2" /f + category: Disable kernel-level Windows Defender drivers + children: + - + name: Disable Windows Defender Firewall Authorization Driver service + docs: http://batcmd.com/windows/10/services/mpsdrv/ + call: + - + function: DisableServiceInRegistry # We must disable it on registry level, "Access is denied" for sc config + parameters: + serviceName: mpsdrv + defaultStartUpMode: 3 # 0: Boot | 1: System | 2: Automatic | 3: Manual | 4: Disabled + - + function: RenameSystemFile + parameters: + filePath: '%SystemRoot%\System32\drivers\mpsdrv.sys' + # - Skipping wdnsfltr "Windows Defender Network Stream Filter Driver" as it's Windows 1709 only + - + name: Disable Microsoft Defender Antivirus Network Inspection System Driver service + docs: http://batcmd.com/windows/10/services/wdnisdrv/ + call: + - + function: RunInlineCodeAsTrustedInstaller + parameters: + # We use "net stop" to stop dependend services as well + code: net stop "WdNisDrv" /yes & sc config "WdNisDrv" start=disabled + revertCode: sc config "WdNisDrv" start=demand & sc start "WdNisDrv" + - + function: RenameSystemFile + parameters: + filePath: '%SystemRoot%\System32\drivers\WdNisDrv.sys' + # - # "Access is denied" when renaming file + # function: RenameSystemFile + # parameters: + # filePath: '%SystemRoot%\System32\drivers\wd\WdNisDrv.sys' + - + name: Disable Microsoft Defender Antivirus Mini-Filter Driver service + docs: + - https://www.n4r1b.com/posts/2020/01/dissecting-the-windows-defender-driver-wdfilter-part-1/ + - http://batcmd.com/windows/10/services/wdfilter/ + call: + - + function: RunInlineCodeAsTrustedInstaller + parameters: + # We use "net stop" to stop dependend services as well + code: sc stop "WdFilter" & sc config "WdFilter" start=disabled + revertCode: sc config "WdFilter" start=boot & sc start "WdFilter" + - + function: RenameSystemFile + parameters: + filePath: '%SystemRoot%\System32\drivers\WdFilter.sys' + # - # "Access is denied" when renaming file + # function: RenameSystemFile + # parameters: + # filePath: '%SystemRoot%\System32\drivers\wd\WdFilter.sys' + - + name: Disable Microsoft Defender Antivirus Boot Driver service + docs: http://batcmd.com/windows/10/services/wdboot/ + call: + - + function: RunInlineCodeAsTrustedInstaller + parameters: + # We use "net stop" to stop dependend services as well + code: sc stop "WdBoot" & sc config "WdBoot" start=disabled + revertCode: sc config "WdBoot" start=boot & sc start "WdBoot" + - + function: RenameSystemFile + parameters: + filePath: '%SystemRoot%\System32\drivers\WdBoot.sys' + # - # "Access is denied" when renaming file + # function: RenameSystemFile + # parameters: + # filePath: '%SystemRoot%\System32\drivers\wd\WdBoot.sys' - name: Disable Microsoft Defender Antivirus Network Inspection service - code: reg add "HKLM\SYSTEM\CurrentControlSet\Services\WdNisSvc" /v "Start" /t REG_DWORD /d "4" /f - revertCode: reg add "HKLM\SYSTEM\CurrentControlSet\Services\WdNisSvc" /v "Start" /t REG_DWORD /d "2" /f + docs: + - http://batcmd.com/windows/10/services/wdnissvc/ + - https://www.howtogeek.com/357184/what-is-microsoft-network-realtime-inspection-service-nissrv.exe-and-why-is-it-running-on-my-pc/ + call: + - + function: RunInlineCodeAsTrustedInstaller + parameters: + # We use "net stop" to stop dependend services as well + code: sc stop "WdNisSvc" & sc config "WdNisSvc" start=disabled + revertCode: sc config "WdNisSvc" start=auto & sc start "WdNisSvc" + # - # "Access is denied" when renaming file + # function: RenameSystemFile + # parameters: + # filePath: '%ProgramFiles%\Windows Defender\NisSrv.exe' # Found also in C:\ProgramData\Microsoft\Windows Defender\Platform\4.18.2107.4-0 and \4.18.2103.7-0 ... - - name: Disable Windows Security service - code: reg add "HKLM\SYSTEM\CurrentControlSet\Services\SecurityHealthService" /v "Start" /t REG_DWORD /d "4" /f - revertCode: reg add "HKLM\SYSTEM\CurrentControlSet\Services\SecurityHealthService" /v "Start" /t REG_DWORD /d "2" /f + name: Disable Windows Defender Advanced Threat Protection Service service + docs: http://batcmd.com/windows/10/services/sense/ + call: + - + function: DisableServiceInRegistry # We must disable it on registry level, "Access is denied" for sc config + parameters: + serviceName: Sense + defaultStartUpMode: 3 # 0: Boot | 1: System | 2: Automatic | 3: Manual | 4: Disabled + - + function: RenameSystemFile + parameters: + filePath: '%ProgramFiles%\Windows Defender Advanced Threat Protection\MsSense.exe' + - + name: Disable Windows Defender Security Center Service + docs: http://batcmd.com/windows/10/services/securityhealthservice/ + call: + - + function: DisableServiceInRegistry # We must disable it on registry level, "Access is denied" for sc config + parameters: + serviceName: SecurityHealthService + defaultStartUpMode: 3 # 0: Boot | 1: System | 2: Automatic | 3: Manual | 4: Disabled + - + function: RenameSystemFile + parameters: + filePath: '%WinDir%\system32\SecurityHealthService.exe' - category: Disable SmartScreen docs: @@ -5714,8 +5847,11 @@ functions: if exist "{{ $filePath }}" ( takeown /f "{{ $filePath }}" icacls "{{ $filePath }}" /grant administrators:F - move "{{ $filePath }}" "{{ $filePath }}.OLD" - echo Moved "{{ $filePath }}" to "{{ $filePath }}.OLD" + move "{{ $filePath }}" "{{ $filePath }}.OLD" && ( + echo Moved "{{ $filePath }}" to "{{ $filePath }}.OLD" + ) || ( + echo Could not move {{ $filePath }} 1>&2 + ) ) else ( echo No action required: {{ $filePath }} is not found. ) @@ -5723,8 +5859,11 @@ functions: if exist "{{ $filePath }}.OLD" ( takeown /f "{{ $filePath }}.OLD" icacls "{{ $filePath }}.OLD" /grant administrators:F - move "{{ $filePath }}.OLD" "{{ $filePath }}" - echo Moved "{{ $filePath }}.OLD" to "{{ $filePath }}" + move "{{ $filePath }}.OLD" "{{ $filePath }}" && ( + echo Moved "{{ $filePath }}.OLD" to "{{ $filePath }}" + ) || ( + echo Could restore from backup file {{ $filePath }}.OLD 1>&2 + ) ) else ( echo Could not find backup file "{{ $filePath }}.OLD" 1>&2 ) @@ -5815,8 +5954,161 @@ functions: - name: RunInlineCode parameters: - - name: code - - name: revertCode - optional: true - code: "{{ $code }}" - revertCode: "{{ $revertCode }}" + - name: code + - name: revertCode + optional: true + code: '{{ $code }}' + revertCode: '{{ with $revertCode }}{{ . }}{{ end }}' + - + name: RunPowerShellWithSameCodeAndRevertCode + parameters: + - name: code + call: + function: RunPowerShell + parameters: + code: '{{ $code }}' + revertCode: '{{ $code }}' + - + name: RunInlineCodeAsTrustedInstaller + parameters: + - name: code + - name: revertCode + call: + function: RunPowerShell + parameters: + code: |- + $trustedInstallerSid = [System.Security.Principal.SecurityIdentifier]::new('S-1-5-80-956008885-3418522649-1831038044-1853292631-2271478464') + $trustedInstallerName = $trustedInstallerSid.Translate([System.Security.Principal.NTAccount]) + $command = '{{ $code }}' + $stdOutFile = New-TemporaryFile + $batchFile = New-TemporaryFile + $powerShellFile = New-TemporaryFile + try { + $batchFile = Rename-Item $batchFile "$($batchFile.BaseName).bat" -PassThru + "@echo off`r`n$command`r`nexit 0" | Out-File $batchFile -Encoding ASCII + $taskName = 'privacy.sexy invoke' + if(Get-ScheduledTask $taskName -ErrorAction Ignore) { # Something may have gone wrong before + Unregister-ScheduledTask $taskName -Confirm:$false + } + $taskAction = New-ScheduledTaskAction ` + -Execute 'cmd.exe' ` + -Argument "cmd /c `"$batchFile`" > $stdOutFile" + $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries + Register-ScheduledTask -TaskName $taskName -Action $taskAction -Settings $settings -Force -ErrorAction Stop | Out-Null + try { + ($scheduleService = New-Object -ComObject Schedule.Service).Connect() + $scheduleService.GetFolder('\').GetTask($taskName).RunEx($null, 0, 0, $trustedInstallerName) | Out-Null + $timeOutLimit = (Get-Date).AddMinutes(5) + Write-Host "Running as $trustedInstallerName" + while((Get-ScheduledTask $taskName).State -eq 'Running') { + Start-Sleep -Milliseconds 200 + if((Get-Date) -gt $timeOutLimit) { + Write-Warning "Skipping results, it took so long to execute script." + break; + } + } + if (($result = (Get-ScheduledTaskInfo $taskName).LastTaskResult) -ne 0) { + Write-Error "Failed to execute with exit code: $result." + } + } finally { + Unregister-ScheduledTask $taskName -Confirm:$false + } + Get-Content $stdOutFile + } finally { + Remove-Item $stdOutFile, $batchFile # + } + revertCode: |- # Duplicated until custom pipes are implemented + $trustedInstallerSid = [System.Security.Principal.SecurityIdentifier]::new('S-1-5-80-956008885-3418522649-1831038044-1853292631-2271478464') + $trustedInstallerName = $trustedInstallerSid.Translate([System.Security.Principal.NTAccount]) + $command = '{{ $revertCode }}' + $stdOutFile = New-TemporaryFile + $batchFile = New-TemporaryFile + try { + $batchFile = Rename-Item $batchFile "$($batchFile.BaseName).bat" -PassThru + "@echo off`r`n$command`r`nexit 0" | Out-File $batchFile -Encoding ASCII + $taskName = 'privacy.sexy invoke' + if(Get-ScheduledTask $taskName -ErrorAction Ignore) { # Something may have gone wrong before + Unregister-ScheduledTask $taskName -Confirm:$false + } + $taskAction = New-ScheduledTaskAction ` + -Execute 'cmd.exe' ` + -Argument "cmd /c `"$batchFile`" > $stdOutFile" + $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries + Register-ScheduledTask -TaskName $taskName -Action $taskAction -Settings $settings -Force -ErrorAction Stop | Out-Null + try { + ($scheduleService = New-Object -ComObject Schedule.Service).Connect() + $scheduleService.GetFolder('\').GetTask($taskName).RunEx($null, 0, 0, $trustedInstallerName) | Out-Null + $timeOutLimit = (Get-Date).AddMinutes(5) + Write-Host "Running as $trustedInstallerName" + while((Get-ScheduledTask $taskName).State -eq 'Running') { + Start-Sleep -Milliseconds 200 + if((Get-Date) -gt $timeOutLimit) { + Write-Warning "Skipping results, it took so long to execute script." + break; + } + } + if (($result = (Get-ScheduledTaskInfo $taskName).LastTaskResult) -ne 0) { + Write-Error "Failed to execute with exit code: $result." + } + } finally { + Unregister-ScheduledTask $taskName -Confirm:$false + } + Get-Content $stdOutFile + } finally { + Remove-Item $stdOutFile, $batchFile + } + - + name: DisableServiceInRegistry + parameters: + - name: serviceName + - name: defaultStartUpMode + call: + function: RunPowerShell + parameters: + code: |- # We do registry way as sc config won't not work + $serviceName = '{{ $serviceName }}' + $service = Get-Service -Name $serviceName -ErrorAction Ignore + if(!$service) { + Write-Host "Service `"$serviceName`" is not found, no action is needed" + exit 0 + } + $name = $service.Name + Stop-Service $name -Force -ErrorAction SilentlyContinue + if($?) { + Write-Host "Stopped `"$name`"" + } else { + Write-Warning "Could not stop `"$name`"" + } + $regKey = "HKLM:\SYSTEM\CurrentControlSet\Services\$name" + if(Test-Path $regKey) { + if( $(Get-ItemProperty -Path "$regKey").Start -eq 4) { + Write-Host "Service `"$name`" is already disabled, no action is needed" + } else { + Set-ItemProperty $regKey -Name Start -Value 4 -Force + Write-Host "Disabled `"$name`"" + } + } else { + Write-Host "Service is not registered at Windows startup, no action is needed." + } + revertCode: |- + $serviceName = '{{ $serviceName }}' + $defaultStartUpMode = '{{ $defaultStartUpMode }}' + $service = Get-Service -Name $serviceName -ErrorAction SilentlyContinue + if(!$service) { + Write-Warning "Service `"$serviceName`" not found" + continue + } + $name = $service.Name + $regKey = "HKLM:\SYSTEM\CurrentControlSet\Services\$name" + if(Test-Path $regKey) { + if( $(Get-ItemProperty -Path "$regKey").Start -eq $defaultStartUpMode) { + Write-Host "Service $serviceName already enabled" + } else { + Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Services\$serviceName" -Name Start -Value $defaultStartUpMode + Write-Host "Enabled service $serviceName (requires reboot)" + } + Set-ItemProperty $regKey -Name Start -Value 0 -Force + Write-Host "Enabled `"$name`", may require restarting your computer." + } else { + Write-Error "Registry key at `"$regKey`" does not exist" + } diff --git a/tests/unit/application/Parser/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShell.spec.ts b/tests/unit/application/Parser/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShell.spec.ts index d883cbd8..0ac595af 100644 --- a/tests/unit/application/Parser/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShell.spec.ts +++ b/tests/unit/application/Parser/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShell.spec.ts @@ -1,12 +1,360 @@ import 'mocha'; import { InlinePowerShell } from '@/application/Parser/Script/Compiler/Expressions/Pipes/PipeDefinitions/InlinePowerShell'; -import { runPipeTests } from './PipeTestRunner'; +import { IPipeTestCase, runPipeTests } from './PipeTestRunner'; describe('InlinePowerShell', () => { // arrange const sut = new InlinePowerShell(); // act runPipeTests(sut, [ + { + name: 'returns undefined when if input is undefined', + input: undefined, + expectedOutput: undefined, + }, + ...prefixTests('newline', getNewLineCases()), + ...prefixTests('comment', getCommentCases()), + ...prefixTests('here-string', hereStringCases()), + ...prefixTests('backtick', backTickCases()), + ]); +}); + +function hereStringCases(): IPipeTestCase[] { + const expectLinesInDoubleQuotes = (...lines: string[]) => lines.join('`r`n'); + const expectLinesInSingleQuotes = (...lines: string[]) => lines.join('\'+"`r`n"+\''); + return [ + { + name: 'adds newlines for double quotes', + input: getWindowsLines( + '@"', + 'Lorem', + 'ipsum', + 'dolor sit amet', + '"@', + ), + expectedOutput: expectLinesInDoubleQuotes( + '"Lorem', + 'ipsum', + 'dolor sit amet"', + ), + }, + { + name: 'adds newlines for single quotes', + input: getWindowsLines( + '@\'', + 'Lorem', + 'ipsum', + 'dolor sit amet', + '\'@', + ), + expectedOutput: expectLinesInSingleQuotes( + '\'Lorem', + 'ipsum', + 'dolor sit amet\'', + ), + }, + { + name: 'does not match with character after here string header', + input: getWindowsLines( + '@" invalid syntax', + 'I will not be processed as here-string', + '"@', + ), + expectedOutput: getSingleLinedOutput( + '@" invalid syntax', + 'I will not be processed as here-string', + '"@', + ), + }, + { + name: 'does not match if there\'s character before here-string terminator', + input: getWindowsLines( + '@\'', + 'do not match here', + ' \'@', + 'character \'@', + ), + expectedOutput: getSingleLinedOutput( + '@\'', + 'do not match here', + ' \'@', + 'character \'@', + ), + }, + { + name: 'does not match with different here-string header/terminator', + input: getWindowsLines( + '@\'', + 'lorem', + '"@', + ), + expectedOutput: getSingleLinedOutput( + '@\'', + 'lorem', + '"@', + ), + }, + { + name: 'matches with inner single quoted here-string', + input: getWindowsLines( + '$hasInnerDoubleQuotedTerminator = @"', + 'inner text', + '@\'', + 'inner terminator text', + '\'@', + '"@', + ), + expectedOutput: expectLinesInDoubleQuotes( + '$hasInnerDoubleQuotedTerminator = "inner text', + '@\'', + 'inner terminator text', + '\'@"', + ), + }, + { + name: 'matches with inner double quoted string', + input: getWindowsLines( + '$hasInnerSingleQuotedTerminator = @\'', + 'inner text', + '@"', + 'inner terminator text', + '"@', + '\'@', + ), + expectedOutput: expectLinesInSingleQuotes( + '$hasInnerSingleQuotedTerminator = \'inner text', + '@"', + 'inner terminator text', + '"@\'', + ), + }, + { + name: 'matches if there\'s character after here-string terminator', + input: getWindowsLines( + '@\'', + 'lorem', + '\'@ after', + ), + expectedOutput: expectLinesInSingleQuotes( + '\'lorem\' after', + ), + }, + { + name: 'escapes double quotes inside double quotes', + input: getWindowsLines( + '@"', + 'For help, type "get-help"', + '"@', + ), + expectedOutput: '"For help, type `"get-help`""', + }, + { + name: 'escapes single quotes inside single quotes', + input: getWindowsLines( + '@\'', + 'For help, type \'get-help\'', + '\'@', + ), + expectedOutput: '\'For help, type \'\'get-help\'\'\'', + }, + { + name: 'converts when here-string header is not at line start', + input: getWindowsLines( + '$page = [XML] @"', + 'multi-lined', + 'and "quoted"', + '"@', + ), + expectedOutput: expectLinesInDoubleQuotes( + '$page = [XML] "multi-lined', + 'and `"quoted`""', + ), + }, + { + name: 'trims after here-string header', + input: getWindowsLines( + '@" \t', + 'text with whitespaces at here-string start', + '"@', + ), + expectedOutput: '"text with whitespaces at here-string start"', + }, + { + name: 'preserves whitespaces in lines', + input: getWindowsLines( + '@\'', + '\ttext with tabs around\t\t', + ' text with whitespaces around ', + '\'@', + ), + expectedOutput: expectLinesInSingleQuotes( + '\'\ttext with tabs around\t\t', + ' text with whitespaces around \'', + ), + }, + ]; +} + +function backTickCases(): IPipeTestCase[] { + return [ + { + name: 'wraps newlines with trailing backtick', + input: getWindowsLines( + 'Get-Service * `', + '| Format-Table -AutoSize', + ), + expectedOutput: 'Get-Service * | Format-Table -AutoSize', + }, + { + name: 'wraps newlines with trailing backtick and different line endings', + input: 'Get-Service `\n' + + '* `\r' + + '| Sort-Object StartType `\r\n' + + '| Format-Table -AutoSize' + , + expectedOutput: 'Get-Service * | Sort-Object StartType | Format-Table -AutoSize', + }, + { + name: 'trims tabs and whitespaces on next lines when wrapping with trailing backtick', + input: getWindowsLines( + 'Get-Service * `', + '\t| Sort-Object StartType `', + ' | Format-Table -AutoSize', + ), + expectedOutput: 'Get-Service * | Sort-Object StartType | Format-Table -AutoSize', + }, + { + name: 'does not wrap without whitespace before backtick', + input: getWindowsLines( + 'Get-Service *`', + '| Format-Table -AutoSize', + ), + expectedOutput: getSingleLinedOutput( + 'Get-Service *`', + '| Format-Table -AutoSize', + ), + }, + { + name: 'does not wrap with characters after', + input: getWindowsLines( + 'line start ` after', + 'should not be wrapped', + ), + expectedOutput: getSingleLinedOutput( + 'line start ` after', + 'should not be wrapped', + ), + }, + ]; +} + +function getCommentCases(): IPipeTestCase[] { + return [ + { + name: 'converts hash comments in the line end', + input: getWindowsLines( + '$text = "Hello"\t# Comment after tab', + '$text+= #Comment without space after hash', + 'Write-Host $text# Comment without space before hash', + ), + expectedOutput: getSingleLinedOutput( + '$text = "Hello"\t<# Comment after tab #>', + '$text+= <# Comment without space after hash #>', + 'Write-Host $text<# Comment without space before hash #>', + ), + }, + { + name: 'converts hash comment line', + input: getWindowsLines( + '# Comment in first line', + 'Write-Host "Hello"', + '# Comment in the middle', + 'Write-Host "World"', + '# Consecutive comments', + '# Last line comment without line ending in the end', + ), + expectedOutput: getSingleLinedOutput( + '<# Comment in first line #>', + 'Write-Host "Hello"', + '<# Comment in the middle #>', + 'Write-Host "World"', + '<# Consecutive comments #>', + '<# Last line comment without line ending in the end #>', + ), + }, + { + name: 'can convert comment with inline comment parts', + input: getWindowsLines( + '$text+= #Comment with < inside', + '$text+= #Comment ending with >', + '$text+= #Comment with <# inline comment #>', + ), + expectedOutput: getSingleLinedOutput( + '$text+= <# Comment with < inside #>', + '$text+= <# Comment ending with > #>', + '$text+= <# Comment with <# inline comment #> #>', + ), + }, + { + name: 'converts empty hash comment', + input: getWindowsLines( + 'Write-Host "Lorem ipsus" #', + 'Write-Host "Non-empty line"', + ), + expectedOutput: getSingleLinedOutput( + 'Write-Host "Lorem ipsus" <##>', + 'Write-Host "Non-empty line"', + ), + }, + { + name: 'adds whitespaces around to match', + input: getWindowsLines( + '#Comment line with no whitespaces around', + 'Write-Host "Hello"#Comment in the end with no whitespaces around', + ), + expectedOutput: getSingleLinedOutput( + '<# Comment line with no whitespaces around #>', + 'Write-Host "Hello"<# Comment in the end with no whitespaces around #>', + ), + }, + { + name: 'trims whitespaces around from match', + input: getWindowsLines( + '# Comment with whitespaces around ', + '#\tComment with tabs around\t\t', + '#\t Comment with tabs and whitespaces around \t \t', + ), + expectedOutput: getSingleLinedOutput( + '<# Comment with whitespaces around #>', + '<# Comment with tabs around #>', + '<# Comment with tabs and whitespaces around #>', + ), + }, + { + name: 'does not convert block comments', + input: getWindowsLines( + '$text = "Hello"\t<# block comment #> + "World"', + '$text = "Hello"\t+<#comment#>"World"', + '<# Block comment in a line #>', + 'Write-Host "Hello world <# Block comment in the end of line #>', + ), + expectedOutput: getSingleLinedOutput( + '$text = "Hello"\t<# block comment #> + "World"', + '$text = "Hello"\t+<#comment#>"World"', + '<# Block comment in a line #>', + 'Write-Host "Hello world <# Block comment in the end of line #>', + ), + }, + { + name: 'does not process if there are no multi lines', + input: 'Write-Host \"expected\" # as it is!', + expectedOutput: 'Write-Host \"expected\" # as it is!', + }, + ]; +} + +function getNewLineCases(): IPipeTestCase[] { + return [ { name: 'no new line', input: 'Write-Host \'Hello, World!\'', @@ -14,34 +362,89 @@ describe('InlinePowerShell', () => { }, { name: '\\n new line', - input: '$things = Get-ChildItem C:\\Windows\\\nforeach ($thing in $things) {\nWrite-Host $thing.Name -ForegroundColor Magenta\n}', - expectedOutput: '$things = Get-ChildItem C:\\Windows\\; foreach ($thing in $things) {; Write-Host $thing.Name -ForegroundColor Magenta; }', + input: + '$things = Get-ChildItem C:\\Windows\\' + + '\nforeach ($thing in $things) {' + + '\nWrite-Host $thing.Name -ForegroundColor Magenta' + + '\n}', + expectedOutput: getSingleLinedOutput( + '$things = Get-ChildItem C:\\Windows\\', + 'foreach ($thing in $things) {', + 'Write-Host $thing.Name -ForegroundColor Magenta', + '}', + ), }, { name: '\\n double empty lines are ignored', - input: '$things = Get-ChildItem C:\\Windows\\\n\nforeach ($thing in $things) {\n\nWrite-Host $thing.Name -ForegroundColor Magenta\n\n\n}', - expectedOutput: '$things = Get-ChildItem C:\\Windows\\; foreach ($thing in $things) {; Write-Host $thing.Name -ForegroundColor Magenta; }', + input: + '$things = Get-ChildItem C:\\Windows\\' + + '\n\nforeach ($thing in $things) {' + + '\n\nWrite-Host $thing.Name -ForegroundColor Magenta' + + '\n\n\n}', + expectedOutput: getSingleLinedOutput( + '$things = Get-ChildItem C:\\Windows\\', + 'foreach ($thing in $things) {', + 'Write-Host $thing.Name -ForegroundColor Magenta', + '}', + ), }, { name: '\\r new line', - input: '$things = Get-ChildItem C:\\Windows\\\rforeach ($thing in $things) {\rWrite-Host $thing.Name -ForegroundColor Magenta\r}', - expectedOutput: '$things = Get-ChildItem C:\\Windows\\; foreach ($thing in $things) {; Write-Host $thing.Name -ForegroundColor Magenta; }', + input: + '$things = Get-ChildItem C:\\Windows\\' + + '\rforeach ($thing in $things) {' + + '\rWrite-Host $thing.Name -ForegroundColor Magenta' + + '\r}', + expectedOutput: getSingleLinedOutput( + '$things = Get-ChildItem C:\\Windows\\', + 'foreach ($thing in $things) {', + 'Write-Host $thing.Name -ForegroundColor Magenta', + '}', + ), }, { name: '\\r and \\n newlines combined', - input: '$things = Get-ChildItem C:\\Windows\\\r\nforeach ($thing in $things) {\n\rWrite-Host $thing.Name -ForegroundColor Magenta\n\r}', - expectedOutput: '$things = Get-ChildItem C:\\Windows\\; foreach ($thing in $things) {; Write-Host $thing.Name -ForegroundColor Magenta; }', + input: + '$things = Get-ChildItem C:\\Windows\\' + + '\r\nforeach ($thing in $things) {' + + '\n\rWrite-Host $thing.Name -ForegroundColor Magenta' + + '\n\r}', + expectedOutput: getSingleLinedOutput( + '$things = Get-ChildItem C:\\Windows\\', + 'foreach ($thing in $things) {', + 'Write-Host $thing.Name -ForegroundColor Magenta', + '}', + ), }, { name: 'trims whitespaces on lines', - input: ' $things = Get-ChildItem C:\\Windows\\ \nforeach ($thing in $things) {\n\tWrite-Host $thing.Name -ForegroundColor Magenta\r \n}', - expectedOutput: '$things = Get-ChildItem C:\\Windows\\; foreach ($thing in $things) {; Write-Host $thing.Name -ForegroundColor Magenta; }', + input: + ' $things = Get-ChildItem C:\\Windows\\ ' + + '\nforeach ($thing in $things) {' + + '\n\tWrite-Host $thing.Name -ForegroundColor Magenta' + + '\r \n}', + expectedOutput: getSingleLinedOutput( + '$things = Get-ChildItem C:\\Windows\\', + 'foreach ($thing in $things) {', + 'Write-Host $thing.Name -ForegroundColor Magenta', + '}', + ), }, - { - name: 'returns undefined when if input is undefined', - input: undefined, - expectedOutput: undefined, - }, - ]); -}); + ]; +} +function prefixTests(prefix: string, tests: IPipeTestCase[]): IPipeTestCase[] { + return tests.map((test) => ({ + name: `[${prefix}] ${test.name}`, + input: test.input, + expectedOutput: test.expectedOutput, + })); +} + +function getWindowsLines(...lines: string[]) { + return lines.join('\r\n'); +} + +function getSingleLinedOutput(...lines: string[]) { + return lines.map((line) => line.trim()).join('; '); +} diff --git a/tests/unit/application/Parser/Script/Compiler/Expressions/Pipes/PipeDefinitions/PipeTestRunner.ts b/tests/unit/application/Parser/Script/Compiler/Expressions/Pipes/PipeDefinitions/PipeTestRunner.ts index 9287168f..2d2b67ca 100644 --- a/tests/unit/application/Parser/Script/Compiler/Expressions/Pipes/PipeDefinitions/PipeTestRunner.ts +++ b/tests/unit/application/Parser/Script/Compiler/Expressions/Pipes/PipeDefinitions/PipeTestRunner.ts @@ -2,13 +2,13 @@ import 'mocha'; import { expect } from 'chai'; import { IPipe } from '@/application/Parser/Script/Compiler/Expressions/Pipes/IPipe'; -interface ITestCase { +export interface IPipeTestCase { readonly name: string; readonly input: string; readonly expectedOutput: string; } -export function runPipeTests(sut: IPipe, testCases: ITestCase[]) { +export function runPipeTests(sut: IPipe, testCases: IPipeTestCase[]) { for (const testCase of testCases) { it(testCase.name, () => { // act