|
| 1 | +name: CI/CD for IIS Module |
| 2 | + |
| 3 | +on: |
| 4 | + push: |
| 5 | + pull_request: |
| 6 | + |
| 7 | +jobs: |
| 8 | + build: |
| 9 | + strategy: |
| 10 | + matrix: |
| 11 | + arch: [x64, x86] |
| 12 | + config: [Release, RelWithDebInfo] |
| 13 | + runs-on: windows-latest |
| 14 | + |
| 15 | + # For Caching |
| 16 | + permissions: |
| 17 | + actions: read |
| 18 | + contents: read |
| 19 | + |
| 20 | + steps: |
| 21 | + - name: Checkout code |
| 22 | + uses: actions/checkout@v5 |
| 23 | + |
| 24 | + - name: Install Apache for x86 |
| 25 | + if: matrix.arch == 'x86' |
| 26 | + shell: pwsh |
| 27 | + run: | |
| 28 | + $apachePath = "${{ github.workspace }}\apache-x86" |
| 29 | + New-Item -ItemType Directory -Path $apachePath -Force |
| 30 | + choco install apache-httpd -y --force --forcex86 --no-progress -r --params="'/installLocation:$apachePath /noService'" |
| 31 | + echo "APACHE_ROOT=$apachePath\Apache24" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append |
| 32 | +
|
| 33 | + - name: Set Apache path for x64 |
| 34 | + if: matrix.arch == 'x64' |
| 35 | + shell: pwsh |
| 36 | + run: | |
| 37 | + echo "APACHE_ROOT=C:\tools\Apache24" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append |
| 38 | + |
| 39 | + - name: Setup MSYS2 |
| 40 | + uses: msys2/setup-msys2@fb197b72ce45fb24f17bf3f807a388985654d1f2 |
| 41 | + with: |
| 42 | + msystem: ${{ matrix.arch == 'x86' && 'MINGW32' || 'UCRT64' }} |
| 43 | + update: true |
| 44 | + install: > |
| 45 | + git |
| 46 | + make |
| 47 | + autoconf |
| 48 | + automake |
| 49 | + libtool |
| 50 | + ${{ matrix.arch == 'x86' && 'mingw-w64-i686-gcc' || 'mingw-w64-ucrt-x86_64-gcc' }} |
| 51 | + ${{ matrix.arch == 'x86' && 'mingw-w64-i686-pkg-config' || 'mingw-w64-ucrt-x86_64-pkg-config' }} |
| 52 | +
|
| 53 | + - name: Clone and build ssdeep |
| 54 | + shell: msys2 {0} |
| 55 | + run: | |
| 56 | + MSYS2_WORKSPACE=$(cygpath -u '${{ github.workspace }}') |
| 57 | +
|
| 58 | + git clone https://github.com/ssdeep-project/ssdeep.git --depth 1 |
| 59 | + cd ssdeep |
| 60 | + autoreconf -i |
| 61 | + |
| 62 | + if [ "${{ matrix.arch }}" = "x86" ]; then |
| 63 | + ./configure --enable-shared --disable-static CFLAGS="-O3" CXXFLAGS="-O3" --build=i686-pc-mingw32 |
| 64 | + else |
| 65 | + ./configure --enable-shared --disable-static CFLAGS="-O3" CXXFLAGS="-O3" |
| 66 | + fi |
| 67 | + |
| 68 | + make dll |
| 69 | +
|
| 70 | + mkdir -p "${MSYS2_WORKSPACE}/ssdeep-install/" |
| 71 | + cp -v fuzzy.dll "${MSYS2_WORKSPACE}/ssdeep-install/" |
| 72 | + cp -v fuzzy.h "${MSYS2_WORKSPACE}/ssdeep-install/" |
| 73 | + cp -v fuzzy.def "${MSYS2_WORKSPACE}/ssdeep-install/" |
| 74 | +
|
| 75 | + - name: Restore vcpkg cache |
| 76 | + id: vcpkg-cache |
| 77 | + uses: TAServers/vcpkg-cache@e848939f754daf406a06006be2e05eb5b17cc481 |
| 78 | + with: |
| 79 | + token: ${{ secrets.GITHUB_TOKEN }} |
| 80 | + prefix: vcpkg-iis-module-${{ matrix.arch }}/ |
| 81 | + |
| 82 | + - uses: ammaraskar/msvc-problem-matcher@1ebcb382869bfdc2cc645e8a2a43b6d319ea1cc0 |
| 83 | + |
| 84 | + - name: Configure CMake for IIS Module |
| 85 | + env: |
| 86 | + VCPKG_FEATURE_FLAGS: "binarycaching" |
| 87 | + VCPKG_BINARY_SOURCES: "clear;files,${{ steps.vcpkg-cache.outputs.path }},readwrite" |
| 88 | + VCPKG_DEFAULT_TRIPLET: ${{ matrix.arch }}-windows |
| 89 | + run: | |
| 90 | + $archFlag = "${{ matrix.arch }}" |
| 91 | + $cmakeArch = if ($archFlag -eq "x86") { "Win32" } else { "x64" } |
| 92 | + $installDir = if ($archFlag -eq "x86") { "x86" } else { "amd64" } |
| 93 | +
|
| 94 | + cmake ` |
| 95 | + -DAPACHE_ROOT="$env:APACHE_ROOT" ` |
| 96 | + -DCMAKE_INSTALL_PREFIX="${{ github.workspace }}\iis\release\$installDir" ` |
| 97 | + -DCMAKE_TOOLCHAIN_FILE="$env:VCPKG_INSTALLATION_ROOT\scripts\buildsystems\vcpkg.cmake" ` |
| 98 | + -DSSDEEP_ROOT="${{ github.workspace }}\ssdeep-install" ` |
| 99 | + -DWITH_SSDEEP=ON ` |
| 100 | + -A $cmakeArch ` |
| 101 | + -DWITH_LUA=ON ` |
| 102 | + -DWITH_YAJL=ON ` |
| 103 | + -S IIS -B "iis\build" |
| 104 | +
|
| 105 | + - name: Build IIS Module |
| 106 | + shell: pwsh |
| 107 | + run: | |
| 108 | + cmake --build "iis\build" --config ${{ matrix.config }} |
| 109 | +
|
| 110 | + - name: Upload artifacts |
| 111 | + uses: actions/upload-artifact@v4 |
| 112 | + with: |
| 113 | + name: iis-module-${{ matrix.arch }}-${{ matrix.config }} |
| 114 | + path: iis/build/${{ matrix.config }}/ |
| 115 | + |
| 116 | + package: |
| 117 | + needs: build |
| 118 | + runs-on: windows-latest |
| 119 | + strategy: |
| 120 | + matrix: |
| 121 | + config: [Release, RelWithDebInfo] |
| 122 | + steps: |
| 123 | + - name: Checkout code |
| 124 | + uses: actions/checkout@v5 |
| 125 | + |
| 126 | + - name: Download x64 artifacts |
| 127 | + uses: actions/download-artifact@v4 |
| 128 | + with: |
| 129 | + name: iis-module-x64-${{ matrix.config }} |
| 130 | + path: iis/release/amd64/ |
| 131 | + |
| 132 | + - name: Download x86 artifacts |
| 133 | + uses: actions/download-artifact@v4 |
| 134 | + with: |
| 135 | + name: iis-module-x86-${{ matrix.config }} |
| 136 | + path: iis/release/x86/ |
| 137 | + |
| 138 | + - name: Generate MSI files |
| 139 | + shell: pwsh |
| 140 | + run: | |
| 141 | + heat dir "iis\release\amd64" -cg ModSec64Components -dr inetsrv64 -gg -sreg -srd -var var.ModSecurityIISRelease64 -out "iis\ModSec64.wxs" |
| 142 | + heat dir "iis\release\x86" -cg ModSec32Components -dr inetsrv32 -gg -sreg -srd -var var.ModSecurityIISRelease32 -out "iis\ModSec32.wxs" |
| 143 | + candle.exe -ext WixUtilExtension -ext WixUIExtension "iis\installer.wxs" "iis\ModSec64.wxs" -arch x64 -dModSecurityIISRelease64="iis\release\amd64\" -out iis\ |
| 144 | + candle.exe -ext WixUtilExtension -ext WixUIExtension "iis\ModSec32.wxs" -arch x86 -dModSecurityIISRelease32="iis\release\x86\" -out iis\ |
| 145 | + light.exe -ext WixUtilExtension -ext WixUIExtension "iis\installer.wixobj" "iis\ModSec32.wixobj" "iis\ModSec64.wixobj" -out "iis\modsecurityiis.msi" |
| 146 | +
|
| 147 | + - name: Upload artifacts |
| 148 | + uses: actions/upload-artifact@v4 |
| 149 | + with: |
| 150 | + name: modsecurityiis-installers-${{ matrix.config }} |
| 151 | + path: iis/modsecurityiis.msi |
| 152 | + |
| 153 | + test: |
| 154 | + needs: package |
| 155 | + runs-on: windows-latest |
| 156 | + strategy: |
| 157 | + matrix: |
| 158 | + config: [Release, RelWithDebInfo] |
| 159 | + steps: |
| 160 | + - name: Checkout code |
| 161 | + uses: actions/checkout@v5 |
| 162 | + |
| 163 | + - name: Download MSI files |
| 164 | + uses: actions/download-artifact@v4 |
| 165 | + with: |
| 166 | + name: modsecurityiis-installers-${{ matrix.config }} |
| 167 | + path: ${{ github.workspace }}/ |
| 168 | + |
| 169 | + - name: Install MSI |
| 170 | + shell: pwsh |
| 171 | + run: | |
| 172 | + $msiPath = "${{ github.workspace }}\modsecurityiis.msi" |
| 173 | + if (-not (Test-Path $msiPath)) { |
| 174 | + Write-Error "MSI file not found at $msiPath" |
| 175 | + exit 1 |
| 176 | + } |
| 177 | + |
| 178 | + # Install with logging for debugging |
| 179 | + $installLog = "${{ github.workspace }}\install.log" |
| 180 | + $installResult = Start-Process -FilePath "msiexec.exe" -ArgumentList @( |
| 181 | + "/i", "`"$msiPath`"", |
| 182 | + "/qn", |
| 183 | + "/norestart", |
| 184 | + "/l*", "`"$installLog`"" |
| 185 | + ) -Wait -PassThru |
| 186 | + |
| 187 | + if ($installResult.ExitCode -ne 0) { |
| 188 | + Write-Error "MSI installation failed with exit code $($installResult.ExitCode)" |
| 189 | + Get-Content $installLog | Write-Host |
| 190 | + exit 1 |
| 191 | + } |
| 192 | + |
| 193 | + $installDir = "C:\Program Files\ModSecurity IIS" |
| 194 | + $requiredFiles = @( |
| 195 | + "modsecurity.conf", |
| 196 | + "modsecurity_iis.conf" |
| 197 | + ) |
| 198 | + |
| 199 | + foreach ($file in $requiredFiles) { |
| 200 | + $filePath = Join-Path $installDir $file |
| 201 | + if (-not (Test-Path $filePath)) { |
| 202 | + Write-Error "Required file $file not found in installation directory" |
| 203 | + exit 1 |
| 204 | + } |
| 205 | + } |
| 206 | +
|
| 207 | + - name: Install OWASP Core Rules |
| 208 | + shell: pwsh |
| 209 | + run: | |
| 210 | + $crsVersion = "v4.18.0" |
| 211 | + $crsUrl = "https://github.com/coreruleset/coreruleset/archive/refs/tags/$crsVersion.tar.gz" |
| 212 | + $crsDir = "C:\Program Files\ModSecurity IIS\coreruleset" |
| 213 | + $modSecurityConfigDir = "C:\Program Files\ModSecurity IIS" |
| 214 | +
|
| 215 | + try { |
| 216 | + New-Item -ItemType Directory -Path $crsDir -Force |
| 217 | + Invoke-WebRequest -Uri $crsUrl -OutFile "$crsDir\$crsVersion.tar.gz" |
| 218 | + tar -xzf "$crsDir\$crsVersion.tar.gz" -C $crsDir --strip-components=1 |
| 219 | +
|
| 220 | + Get-ChildItem "$crsDir" -Recurse -Filter "*.example" | ForEach-Object { |
| 221 | + $newName = $_.Name.Replace(".example", "") |
| 222 | + Rename-Item -Path $_.FullName -NewName $newName |
| 223 | + } |
| 224 | + |
| 225 | + $modSecurityConfigFile = "$modSecurityConfigDir\modsecurity_iis.conf" |
| 226 | +
|
| 227 | + $crsRules = @( |
| 228 | + "Include coreruleset/crs-setup.conf", |
| 229 | + "Include coreruleset/plugins/*-config.conf", |
| 230 | + "Include coreruleset/plugins/*-before.conf", |
| 231 | + "Include coreruleset/rules/*.conf", |
| 232 | + "Include coreruleset/plugins/*-after.conf" |
| 233 | + ) |
| 234 | +
|
| 235 | + Add-Content -Path $modSecurityConfigFile -Value $crsRules |
| 236 | +
|
| 237 | + (Get-Content -Path $modSecurityConfigDir\modsecurity.conf) -replace 'SecRuleEngine DetectionOnly', 'SecRuleEngine On' | Set-Content -Path $modSecurityConfigDir\modsecurity.conf |
| 238 | +
|
| 239 | + } |
| 240 | + catch { |
| 241 | + Write-Error "Failed to install OWASP Core Rules: $($_.Exception.Message)" |
| 242 | + exit 1 |
| 243 | + } |
| 244 | +
|
| 245 | + - name: Test IIS Module |
| 246 | + shell: pwsh |
| 247 | + run: | |
| 248 | + $iisConfigDir = "C:\Program Files\ModSecurity IIS\" |
| 249 | +
|
| 250 | + Restart-Service W3SVC -Force |
| 251 | +
|
| 252 | + $modules = & "$env:SystemRoot\system32\inetsrv\appcmd.exe" list modules |
| 253 | + Write-Host "IIS modules: $modules" |
| 254 | + if ($LASTEXITCODE -ne 0) { |
| 255 | + Write-Error "appcmd failed with exit code $LASTEXITCODE" |
| 256 | + exit 1 |
| 257 | + } |
| 258 | +
|
| 259 | + if (-not ($modules -match "ModSecurity")) { |
| 260 | + Write-Error "ModSecurity module not found in IIS modules" |
| 261 | + Write-Host "IIS modules: $modules" |
| 262 | + exit 1 |
| 263 | + } |
| 264 | +
|
| 265 | + $testCases = @( |
| 266 | + @{Url = "http://localhost/"; Description = "Normal request"; ExpectedCode = 200}, |
| 267 | + @{Url = "http://localhost/?id=1' OR '1'='1"; Description = "SQL injection attempt"; ExpectedCode = 403}, |
| 268 | + @{Url = "http://localhost/?q=<script>alert('test')</script>"; Description = "XSS attempt"; ExpectedCode = 403} |
| 269 | + ) |
| 270 | + |
| 271 | + foreach ($test in $testCases) { |
| 272 | + try { |
| 273 | + $response = Invoke-WebRequest $test.Url -UseBasicParsing -SkipHttpErrorCheck -TimeoutSec 30 |
| 274 | + |
| 275 | + if ($response.StatusCode -eq $test.ExpectedCode) { |
| 276 | + Write-Host "PASS: $($test.Description) - returned $($response.StatusCode)" |
| 277 | + } |
| 278 | + else { |
| 279 | + Write-Host "FAIL: $($test.Description) - expected $($test.ExpectedCode) but got $($response.StatusCode)" |
| 280 | + } |
| 281 | + } |
| 282 | + catch { |
| 283 | + Write-Host "ERROR: $($test.Description) - request failed: $($_.Exception.Message)" |
| 284 | + } |
| 285 | + } |
| 286 | + |
| 287 | +
|
| 288 | + # Check event log |
| 289 | + $badMessagePattern = 'Failed to find the RegisterModule entrypoint|The description for Event ID|The data is the error|dll failed to load' |
| 290 | +
|
| 291 | + $events = Get-EventLog -LogName Application -Newest 100 | |
| 292 | + Where-Object { $_.Message -match $badMessagePattern } | |
| 293 | + Where-Object { $_.Source -match 'IIS|W3SVC|mscor|IIS-W3SVC|IIS-W3WP|ModSecurity' } |
| 294 | +
|
| 295 | + if ($events -and $events.Count -gt 0) { |
| 296 | + Write-Host '::error:: Found errors in event log' |
| 297 | + $events | Select-Object TimeGenerated, Source, EntryType, EventID, Message | Format-List |
| 298 | + Exit 1 |
| 299 | + } |
| 300 | +
|
| 301 | + Get-EventLog -LogName Application -Source ModSecurity | Format-List |
| 302 | +
|
| 303 | + - name: Install go-ftw |
| 304 | + shell: pwsh |
| 305 | + run: | |
| 306 | + go install github.com/coreruleset/go-ftw@latest |
| 307 | +
|
| 308 | + # Certain rules are disabled due to specific IIS behavior patterns. |
| 309 | + # Using go-ftw in cloud mode as the IIS connector does not generate logs in file format. |
| 310 | + # Technically, Event logs can be streamed to files, but this requires implementing rate limits to avoid log overflow. |
| 311 | + - name: Test ModSecurity Rules |
| 312 | + shell: pwsh |
| 313 | + run: | |
| 314 | + $testRuleDir = "C:\Program Files\ModSecurity IIS\coreruleset\tests\regression\tests" |
| 315 | + $goBinPath = "" |
| 316 | + if ($env:GOBIN) { |
| 317 | + $goBinPath = $env:GOBIN |
| 318 | + } elseif ($env:GOPATH) { |
| 319 | + $goBinPath = Join-Path $env:GOPATH "bin" |
| 320 | + } else { |
| 321 | + $goBinPath = Join-Path $env:USERPROFILE "go\bin" |
| 322 | + } |
| 323 | +
|
| 324 | + & "$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 |
| 325 | +
|
0 commit comments