From ab8bce768650a10677f0a13b3a9fae93c83802ff Mon Sep 17 00:00:00 2001 From: undergroundwires Date: Wed, 20 Oct 2021 21:12:47 +0200 Subject: [PATCH] Support disabling of protected services #74 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add new ways to disable Defender on Windows: 1. Disable through renaming required files 2. Disable using registry changes 3. Disable using TrustedInstaller user Add support for running code as TrustedInstaller 🥳. It allows running commands in OS-protected areas. It is written in PowerShell and it uses PowerShell syntax like backticks that are inlined in special way. So the commit extends inlining support and allows writing PowerShell using: - Comments - Here-strings - Backticks Add disabling of more Defender service Improve documentation and categorization of services. --- .../Pipes/PipeDefinitions/InlinePowerShell.ts | 98 +++- .../Parser/Script/Syntax/BatchFileSyntax.ts | 2 +- src/application/collections/windows.yaml | 356 ++++++++++++-- .../PipeDefinitions/InlinePowerShell.spec.ts | 439 +++++++++++++++++- .../Pipes/PipeDefinitions/PipeTestRunner.ts | 4 +- 5 files changed, 842 insertions(+), 57 deletions(-) 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