name: CI/CD for IIS Module

on:
  push:
  pull_request:

jobs:
  build:
    strategy:
      matrix:
        arch: [x64, x86]
        config: [Release, RelWithDebInfo]
    runs-on: windows-latest
    
    # For Caching
    permissions:
      actions: read
      contents: read

    steps:
    - name: Checkout code
      uses: actions/checkout@v5
      with:
        submodules: recursive
        fetch-depth: 0
    - name: Install Apache for x86
      if: matrix.arch == 'x86'
      shell: pwsh
      run: |
        $apachePath = "${{ github.workspace }}\apache-x86"
        New-Item -ItemType Directory -Path $apachePath -Force
        choco install apache-httpd -y --force --forcex86 --no-progress -r --params="'/installLocation:$apachePath /noService'"
        echo "APACHE_ROOT=$apachePath\Apache24" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append

    - name: Set Apache path for x64
      if: matrix.arch == 'x64'
      shell: pwsh
      run: |
        echo "APACHE_ROOT=C:\tools\Apache24" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
    
    - name: Setup MSYS2
      uses: msys2/setup-msys2@fb197b72ce45fb24f17bf3f807a388985654d1f2
      with:
        msystem: ${{ matrix.arch == 'x86' && 'MINGW32' || 'UCRT64' }}
        update: true
        install: >
          git
          make
          autoconf
          automake
          libtool
          ${{ matrix.arch == 'x86' && 'mingw-w64-i686-gcc' || 'mingw-w64-ucrt-x86_64-gcc' }}
          ${{ matrix.arch == 'x86' && 'mingw-w64-i686-pkg-config' || 'mingw-w64-ucrt-x86_64-pkg-config' }}

    - name: Clone and build ssdeep
      shell: msys2 {0}
      run: |
        MSYS2_WORKSPACE=$(cygpath -u '${{ github.workspace }}')

        git clone https://github.com/ssdeep-project/ssdeep.git --depth 1
        cd ssdeep
        autoreconf -i
        
        if [ "${{ matrix.arch }}" = "x86" ]; then
          ./configure --enable-shared --disable-static CFLAGS="-O3" CXXFLAGS="-O3" --build=i686-pc-mingw32
        else
          ./configure --enable-shared --disable-static CFLAGS="-O3" CXXFLAGS="-O3"
        fi
        
        make dll

        mkdir -p "${MSYS2_WORKSPACE}/ssdeep-install/"
        cp -v fuzzy.dll "${MSYS2_WORKSPACE}/ssdeep-install/"
        cp -v fuzzy.h "${MSYS2_WORKSPACE}/ssdeep-install/"
        cp -v fuzzy.def "${MSYS2_WORKSPACE}/ssdeep-install/"

    - name: Restore vcpkg cache
      id: vcpkg-cache
      uses: TAServers/vcpkg-cache@e848939f754daf406a06006be2e05eb5b17cc481
      with:
        token: ${{ secrets.GITHUB_TOKEN }}
        prefix: vcpkg-iis-module-${{ matrix.arch }}/

    - uses: ammaraskar/msvc-problem-matcher@1ebcb382869bfdc2cc645e8a2a43b6d319ea1cc0

    - name: Configure CMake for IIS Module
      env:
        VCPKG_FEATURE_FLAGS: "binarycaching"
        VCPKG_BINARY_SOURCES: "clear;files,${{ steps.vcpkg-cache.outputs.path }},readwrite"
        VCPKG_DEFAULT_TRIPLET: ${{ matrix.arch }}-windows
      run: |
        $archFlag = "${{ matrix.arch }}"
        $cmakeArch = if ($archFlag -eq "x86") { "Win32" } else { "x64" }
        $installDir = if ($archFlag -eq "x86") { "x86" } else { "amd64" }

        cmake `
        -DAPACHE_ROOT="$env:APACHE_ROOT" `
        -DCMAKE_INSTALL_PREFIX="${{ github.workspace }}\iis\release\$installDir" `
        -DCMAKE_TOOLCHAIN_FILE="$env:VCPKG_INSTALLATION_ROOT\scripts\buildsystems\vcpkg.cmake" `
        -DSSDEEP_ROOT="${{ github.workspace }}\ssdeep-install" `
        -DWITH_SSDEEP=ON `
        -A $cmakeArch `
        -DWITH_LUA=ON `
        -DWITH_YAJL=ON `
        -S IIS -B "iis\build"

    - name: Build IIS Module
      shell: pwsh
      run: |
        cmake --build "iis\build" --config ${{ matrix.config }}

    - name: Upload artifacts
      uses: actions/upload-artifact@v4
      with:
        name: iis-module-${{ matrix.arch }}-${{ matrix.config }}
        path: iis/build/${{ matrix.config }}/
    
  package:
    needs: build
    runs-on: windows-latest
    strategy:
      matrix:
        config: [Release, RelWithDebInfo]
    steps:
    - name: Checkout code
      uses: actions/checkout@v5
      with:
        submodules: recursive
        fetch-depth: 0

    - name: Download x64 artifacts
      uses: actions/download-artifact@v4
      with:
        name: iis-module-x64-${{ matrix.config }}
        path: iis/release/amd64/

    - name: Download x86 artifacts
      uses: actions/download-artifact@v4
      with:
        name: iis-module-x86-${{ matrix.config }}
        path: iis/release/x86/

    - name: Generate MSI files
      shell: pwsh
      run: |
        heat dir "iis\release\amd64" -cg ModSec64Components -dr inetsrv64 -gg -sreg -srd -var var.ModSecurityIISRelease64 -out "iis\ModSec64.wxs"
        heat dir "iis\release\x86" -cg ModSec32Components -dr inetsrv32 -gg -sreg -srd -var var.ModSecurityIISRelease32 -out "iis\ModSec32.wxs"
        candle.exe -ext WixUtilExtension -ext WixUIExtension "iis\installer.wxs" "iis\ModSec64.wxs" -arch x64 -dModSecurityIISRelease64="iis\release\amd64\" -out iis\
        candle.exe -ext WixUtilExtension -ext WixUIExtension "iis\ModSec32.wxs" -arch x86 -dModSecurityIISRelease32="iis\release\x86\" -out iis\
        light.exe -ext WixUtilExtension -ext WixUIExtension "iis\installer.wixobj" "iis\ModSec32.wixobj" "iis\ModSec64.wixobj" -out "iis\modsecurityiis.msi"

    - name: Upload artifacts
      uses: actions/upload-artifact@v4
      with:
        name: modsecurityiis-installers-${{ matrix.config }}
        path: iis/modsecurityiis.msi

  test:
    needs: package
    runs-on: windows-latest
    strategy:
      matrix:
        config: [Release, RelWithDebInfo]
    steps:
    - name: Checkout code
      uses: actions/checkout@v5
      with:
        submodules: recursive
        fetch-depth: 0

    - name: Download MSI files
      uses: actions/download-artifact@v4
      with:
        name: modsecurityiis-installers-${{ matrix.config }}
        path: ${{ github.workspace }}/

    - name: Install MSI
      shell: pwsh
      run: |
        $msiPath = "${{ github.workspace }}\modsecurityiis.msi"
        if (-not (Test-Path $msiPath)) {
          Write-Error "MSI file not found at $msiPath"
          exit 1
        }
        
        # Install with logging for debugging
        $installLog = "${{ github.workspace }}\install.log"
        $installResult = Start-Process -FilePath "msiexec.exe" -ArgumentList @(
          "/i", "`"$msiPath`"",
          "/qn",
          "/norestart",
          "/l*", "`"$installLog`""
        ) -Wait -PassThru
        
        if ($installResult.ExitCode -ne 0) {
          Write-Error "MSI installation failed with exit code $($installResult.ExitCode)"
          Get-Content $installLog | Write-Host
          exit 1
        }
        
        $installDir = "C:\Program Files\ModSecurity IIS"
        $requiredFiles = @(
          "modsecurity.conf",
          "modsecurity_iis.conf"
        )
        
        foreach ($file in $requiredFiles) {
          $filePath = Join-Path $installDir $file
          if (-not (Test-Path $filePath)) {
            Write-Error "Required file $file not found in installation directory"
            exit 1
          }
        }

    - name: Install OWASP Core Rules
      shell: pwsh
      run: |
        $crsVersion = "v4.18.0"
        $crsUrl = "https://github.com/coreruleset/coreruleset/archive/refs/tags/$crsVersion.tar.gz"
        $crsDir = "C:\Program Files\ModSecurity IIS\coreruleset"
        $modSecurityConfigDir = "C:\Program Files\ModSecurity IIS"

        try {
          New-Item -ItemType Directory -Path $crsDir -Force
          Invoke-WebRequest -Uri $crsUrl -OutFile "$crsDir\$crsVersion.tar.gz"
          tar -xzf "$crsDir\$crsVersion.tar.gz" -C $crsDir --strip-components=1

          Get-ChildItem "$crsDir" -Recurse -Filter "*.example" | ForEach-Object {
              $newName = $_.Name.Replace(".example", "")
              Rename-Item -Path $_.FullName -NewName $newName
          }
          
          $modSecurityConfigFile = "$modSecurityConfigDir\modsecurity_iis.conf"

          $crsRules = @(
            "Include coreruleset/crs-setup.conf",
            "Include coreruleset/plugins/*-config.conf",
            "Include coreruleset/plugins/*-before.conf",
            "Include coreruleset/rules/*.conf",
            "Include coreruleset/plugins/*-after.conf"
          )

          Add-Content -Path $modSecurityConfigFile -Value $crsRules

          (Get-Content -Path $modSecurityConfigDir\modsecurity.conf) -replace 'SecRuleEngine DetectionOnly', 'SecRuleEngine On' | Set-Content -Path $modSecurityConfigDir\modsecurity.conf

        }
        catch {
            Write-Error "Failed to install OWASP Core Rules: $($_.Exception.Message)"
            exit 1
        }

    - name: Test IIS Module
      shell: pwsh
      run: |
        $iisConfigDir = "C:\Program Files\ModSecurity IIS\"

        Restart-Service W3SVC -Force

        $modules = & "$env:SystemRoot\system32\inetsrv\appcmd.exe" list modules
        Write-Host "IIS modules: $modules"
        if ($LASTEXITCODE -ne 0) {
            Write-Error "appcmd failed with exit code $LASTEXITCODE"
            exit 1
        }

        if (-not ($modules -match "ModSecurity")) {
            Write-Error "ModSecurity module not found in IIS modules"
            Write-Host "IIS modules: $modules"
            exit 1
        }

        $testCases = @(
            @{Url = "http://localhost/"; Description = "Normal request"; ExpectedCode = 200},
            @{Url = "http://localhost/?id=1' OR '1'='1"; Description = "SQL injection attempt"; ExpectedCode = 403},
            @{Url = "http://localhost/?q=<script>alert('test')</script>"; Description = "XSS attempt"; ExpectedCode = 403}
        )
        
        foreach ($test in $testCases) {
            try {
                $response = Invoke-WebRequest $test.Url -UseBasicParsing -SkipHttpErrorCheck -TimeoutSec 30
                
                if ($response.StatusCode -eq $test.ExpectedCode) {
                    Write-Host "PASS: $($test.Description) - returned $($response.StatusCode)"
                }
                else {
                    Write-Host "FAIL: $($test.Description) - expected $($test.ExpectedCode) but got $($response.StatusCode)"
                }
            }
            catch {
                Write-Host "ERROR: $($test.Description) - request failed: $($_.Exception.Message)"
            }
        }
        

        # Check event log
        $badMessagePattern = 'Failed to find the RegisterModule entrypoint|The description for Event ID|The data is the error|dll failed to load'

        $events = Get-EventLog -LogName Application -Newest 100 |
            Where-Object { $_.Message -match $badMessagePattern } |
            Where-Object { $_.Source -match 'IIS|W3SVC|mscor|IIS-W3SVC|IIS-W3WP|ModSecurity' }

        if ($events -and $events.Count -gt 0) {
            Write-Host '::error:: Found errors in event log'
            $events | Select-Object TimeGenerated, Source, EntryType, EventID, Message | Format-List
            Exit 1
        }

        Get-EventLog -LogName Application -Source ModSecurity | Format-List

    - name: Install go-ftw
      shell: pwsh
      run: |
        go install github.com/coreruleset/go-ftw@latest

    # Certain rules are disabled due to specific IIS behavior patterns.
    # Using go-ftw in cloud mode as the IIS connector does not generate logs in file format.
    # Technically, Event logs can be streamed to files, but this requires implementing rate limits to avoid log overflow.
    - name: Test ModSecurity Rules
      shell: pwsh
      run: |
        $testRuleDir = "C:\Program Files\ModSecurity IIS\coreruleset\tests\regression\tests"
        $goBinPath = ""
        if ($env:GOBIN) {
            $goBinPath = $env:GOBIN
        } elseif ($env:GOPATH) {
            $goBinPath = Join-Path $env:GOPATH "bin"
        } else {
            $goBinPath = Join-Path $env:USERPROFILE "go\bin"
        }

        & "$goBinPath\go-ftw.exe" run -d $testRuleDir --cloud -e "920100-2$|920100-4$|920100-8$|920100-12$|920272-5$|920290-1$|920620-1$|920380-1$" --show-failures-only

