├── .github ├── dependabot.yml └── workflows │ ├── autorelease-tag.yml │ ├── build-test.yml │ ├── codeql-analysis.yml │ ├── dep-auto-merge.yml │ ├── fingerprint-update.yml │ └── lint-test.yml ├── .gitignore ├── LICENSE.md ├── README.md ├── categories_data.json ├── cmd └── update-fingerprints │ └── main.go ├── examples └── main.go ├── fingerprint_body.go ├── fingerprint_cookies.go ├── fingerprint_headers.go ├── fingerprints.go ├── fingerprints_data.go ├── fingerprints_data.json ├── fingerprints_test.go ├── go.mod ├── go.sum ├── patterns.go ├── patterns_test.go ├── tech.go └── wappalyzergo_test.go /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | 9 | # Maintain dependencies for GitHub Actions 10 | - package-ecosystem: "github-actions" 11 | directory: "/" 12 | schedule: 13 | interval: "weekly" 14 | target-branch: "main" 15 | commit-message: 16 | prefix: "chore" 17 | include: "scope" 18 | 19 | # Maintain dependencies for go modules 20 | - package-ecosystem: "gomod" 21 | directory: "/" 22 | schedule: 23 | interval: "weekly" 24 | target-branch: "main" 25 | commit-message: 26 | prefix: "chore" 27 | include: "scope" 28 | 29 | # Maintain dependencies for docker 30 | - package-ecosystem: "docker" 31 | directory: "/" 32 | schedule: 33 | interval: "weekly" 34 | target-branch: "main" 35 | commit-message: 36 | prefix: "chore" 37 | include: "scope" 38 | -------------------------------------------------------------------------------- /.github/workflows/autorelease-tag.yml: -------------------------------------------------------------------------------- 1 | name: 🔖 Release Tag 2 | 3 | on: 4 | workflow_run: 5 | workflows: ["💡Fingerprints Update"] 6 | types: 7 | - completed 8 | workflow_dispatch: 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | with: 16 | fetch-depth: 0 17 | 18 | - name: Get Commit Count 19 | id: get_commit 20 | run: git rev-list `git rev-list --tags --no-walk --max-count=1`..HEAD --count | xargs -I {} echo COMMIT_COUNT={} >> $GITHUB_OUTPUT 21 | 22 | - name: Create release and tag 23 | if: ${{ steps.get_commit.outputs.COMMIT_COUNT > 0 }} 24 | id: tag_version 25 | uses: mathieudutour/github-tag-action@v6.2 26 | with: 27 | github_token: ${{ secrets.GITHUB_TOKEN }} 28 | 29 | - name: Create a GitHub release 30 | if: ${{ steps.get_commit.outputs.COMMIT_COUNT > 0 }} 31 | uses: actions/create-release@v1 32 | env: 33 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 34 | with: 35 | tag_name: ${{ steps.tag_version.outputs.new_tag }} 36 | release_name: Release ${{ steps.tag_version.outputs.new_tag }} 37 | body: ${{ steps.tag_version.outputs.changelog }} -------------------------------------------------------------------------------- /.github/workflows/build-test.yml: -------------------------------------------------------------------------------- 1 | name: 🔨 Build Test 2 | on: 3 | push: 4 | pull_request: 5 | workflow_dispatch: 6 | 7 | 8 | jobs: 9 | build: 10 | name: Test Builds 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Set up Go 14 | uses: actions/setup-go@v5 15 | with: 16 | go-version: 1.21.x 17 | 18 | - name: Check out code 19 | uses: actions/checkout@v4 20 | 21 | - name: Test 22 | run: go test ./... -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: 🚨 CodeQL Analysis 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | branches: 7 | - dev 8 | 9 | jobs: 10 | analyze: 11 | name: Analyze 12 | runs-on: ubuntu-latest 13 | permissions: 14 | actions: read 15 | contents: read 16 | security-events: write 17 | 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | language: [ 'go' ] 22 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 23 | 24 | steps: 25 | - name: Checkout repository 26 | uses: actions/checkout@v4 27 | 28 | # Initializes the CodeQL tools for scanning. 29 | - name: Initialize CodeQL 30 | uses: github/codeql-action/init@v3 31 | with: 32 | languages: ${{ matrix.language }} 33 | 34 | - name: Autobuild 35 | uses: github/codeql-action/autobuild@v3 36 | 37 | - name: Perform CodeQL Analysis 38 | uses: github/codeql-action/analyze@v3 -------------------------------------------------------------------------------- /.github/workflows/dep-auto-merge.yml: -------------------------------------------------------------------------------- 1 | name: 🤖 dep auto merge 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | workflow_dispatch: 8 | 9 | permissions: 10 | pull-requests: write 11 | issues: write 12 | repository-projects: write 13 | 14 | jobs: 15 | automerge: 16 | runs-on: ubuntu-latest 17 | if: github.actor == 'dependabot[bot]' 18 | steps: 19 | - uses: actions/checkout@v4 20 | with: 21 | token: ${{ secrets.DEPENDABOT_PAT }} 22 | 23 | - uses: ahmadnassri/action-dependabot-auto-merge@v2 24 | with: 25 | github-token: ${{ secrets.DEPENDABOT_PAT }} 26 | target: all -------------------------------------------------------------------------------- /.github/workflows/fingerprint-update.yml: -------------------------------------------------------------------------------- 1 | name: 💡Fingerprints Update 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: '0 0 * * 0' 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout Repo 13 | uses: actions/checkout@v4 14 | with: 15 | persist-credentials: false 16 | 17 | - name: Setup golang 18 | uses: actions/setup-go@v5 19 | with: 20 | go-version: 1.21.x 21 | 22 | - name: Installing Update binary 23 | run: | 24 | go install github.com/projectdiscovery/wappalyzergo/cmd/update-fingerprints 25 | shell: bash 26 | 27 | - name: Downloading latest wappalyzer changes 28 | run: | 29 | update-fingerprints -fingerprints fingerprints_data.json 30 | shell: bash 31 | 32 | - name: Create local changes 33 | run: | 34 | git add fingerprints_data.json 35 | 36 | - name: Commit files 37 | run: | 38 | git config --local user.email "action@github.com" 39 | git config --local user.name "GitHub Action" 40 | git commit -m "Weekly fingerprints update [$(date)] :robot:" -a --allow-empty 41 | 42 | - name: Push changes 43 | uses: ad-m/github-push-action@master 44 | with: 45 | github_token: ${{ secrets.GITHUB_TOKEN }} 46 | branch: ${{ github.ref }} -------------------------------------------------------------------------------- /.github/workflows/lint-test.yml: -------------------------------------------------------------------------------- 1 | name: 🙏🏻 Lint Test 2 | on: 3 | push: 4 | pull_request: 5 | workflow_dispatch: 6 | 7 | jobs: 8 | lint: 9 | name: Lint Test 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v4 14 | - name: Set up Go 15 | uses: actions/setup-go@v5 16 | with: 17 | go-version: 1.21.x 18 | - name: Run golangci-lint 19 | uses: golangci/golangci-lint-action@v6.5.2 20 | with: 21 | version: latest 22 | args: --timeout 5m 23 | working-directory: . 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | cmd/update-fingerprints/update-fingerprints 3 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 ProjectDiscovery, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Wappalyzergo 2 | 3 | A high performance port of the Wappalyzer Technology Detection Library to Go. Inspired by [Webanalyze](https://github.com/rverton/webanalyze). 4 | 5 | Uses data from https://github.com/AliasIO/wappalyzer 6 | 7 | ## Features 8 | 9 | - Very simple and easy to use, with clean codebase. 10 | - Normalized regexes + auto-updating database of wappalyzer fingerprints. 11 | - Optimized for performance: parsing HTML manually for best speed. 12 | 13 | ### Using *go install* 14 | 15 | ```sh 16 | go install -v github.com/projectdiscovery/wappalyzergo/cmd/update-fingerprints@latest 17 | ``` 18 | 19 | After this command *wappalyzergo* library source will be in your current go.mod. 20 | 21 | ## Example 22 | Usage Example: 23 | 24 | ``` go 25 | package main 26 | 27 | import ( 28 | "fmt" 29 | "io" 30 | "log" 31 | "net/http" 32 | 33 | wappalyzer "github.com/projectdiscovery/wappalyzergo" 34 | ) 35 | 36 | func main() { 37 | resp, err := http.DefaultClient.Get("https://www.hackerone.com") 38 | if err != nil { 39 | log.Fatal(err) 40 | } 41 | data, _ := io.ReadAll(resp.Body) // Ignoring error for example 42 | 43 | wappalyzerClient, err := wappalyzer.New() 44 | fingerprints := wappalyzerClient.Fingerprint(resp.Header, data) 45 | fmt.Printf("%v\n", fingerprints) 46 | 47 | // Output: map[Acquia Cloud Platform:{} Amazon EC2:{} Apache:{} Cloudflare:{} Drupal:{} PHP:{} Percona:{} React:{} Varnish:{}] 48 | } 49 | ``` 50 | -------------------------------------------------------------------------------- /categories_data.json: -------------------------------------------------------------------------------- 1 | { 2 | "1": { 3 | "groups": [ 4 | 3 5 | ], 6 | "name": "CMS", 7 | "priority": 1 8 | }, 9 | "2": { 10 | "groups": [ 11 | 3, 12 | 4, 13 | 18 14 | ], 15 | "name": "Message boards", 16 | "priority": 1 17 | }, 18 | "3": { 19 | "groups": [ 20 | 5 21 | ], 22 | "name": "Database managers", 23 | "priority": 2 24 | }, 25 | "4": { 26 | "groups": [ 27 | 3 28 | ], 29 | "name": "Documentation", 30 | "priority": 2 31 | }, 32 | "5": { 33 | "groups": [ 34 | 6 35 | ], 36 | "name": "Widgets", 37 | "priority": 9 38 | }, 39 | "6": { 40 | "groups": [ 41 | 1 42 | ], 43 | "name": "Ecommerce", 44 | "priority": 1 45 | }, 46 | "7": { 47 | "groups": [ 48 | 3, 49 | 10 50 | ], 51 | "name": "Photo galleries", 52 | "priority": 1 53 | }, 54 | "8": { 55 | "groups": [ 56 | 3 57 | ], 58 | "name": "Wikis", 59 | "priority": 1 60 | }, 61 | "9": { 62 | "groups": [ 63 | 5, 64 | 7 65 | ], 66 | "name": "Hosting panels", 67 | "priority": 2 68 | }, 69 | "10": { 70 | "groups": [ 71 | 8 72 | ], 73 | "name": "Analytics", 74 | "priority": 9 75 | }, 76 | "11": { 77 | "groups": [ 78 | 3 79 | ], 80 | "name": "Blogs", 81 | "priority": 1 82 | }, 83 | "12": { 84 | "groups": [ 85 | 9 86 | ], 87 | "name": "JavaScript frameworks", 88 | "priority": 8 89 | }, 90 | "13": { 91 | "groups": [ 92 | 3, 93 | 18 94 | ], 95 | "name": "Issue trackers", 96 | "priority": 2 97 | }, 98 | "14": { 99 | "groups": [ 100 | 10 101 | ], 102 | "name": "Video players", 103 | "priority": 7 104 | }, 105 | "15": { 106 | "groups": [ 107 | 3, 108 | 18 109 | ], 110 | "name": "Comment systems", 111 | "priority": 9 112 | }, 113 | "16": { 114 | "groups": [ 115 | 11 116 | ], 117 | "name": "Security", 118 | "priority": 9 119 | }, 120 | "17": { 121 | "groups": [ 122 | 9 123 | ], 124 | "name": "Font scripts", 125 | "priority": 9 126 | }, 127 | "18": { 128 | "groups": [ 129 | 9 130 | ], 131 | "name": "Web frameworks", 132 | "priority": 7 133 | }, 134 | "19": { 135 | "groups": [ 136 | 6 137 | ], 138 | "name": "Miscellaneous", 139 | "priority": 10 140 | }, 141 | "20": { 142 | "groups": [ 143 | 9 144 | ], 145 | "name": "Editors", 146 | "priority": 4 147 | }, 148 | "21": { 149 | "groups": [ 150 | 3 151 | ], 152 | "name": "LMS", 153 | "priority": 1 154 | }, 155 | "22": { 156 | "groups": [ 157 | 7 158 | ], 159 | "name": "Web servers", 160 | "priority": 8 161 | }, 162 | "23": { 163 | "groups": [ 164 | 7 165 | ], 166 | "name": "Caching", 167 | "priority": 7 168 | }, 169 | "24": { 170 | "groups": [ 171 | 3 172 | ], 173 | "name": "Rich text editors", 174 | "priority": 5 175 | }, 176 | "25": { 177 | "groups": [ 178 | 9 179 | ], 180 | "name": "JavaScript graphics", 181 | "priority": 6 182 | }, 183 | "26": { 184 | "groups": [ 185 | 9 186 | ], 187 | "name": "Mobile frameworks", 188 | "priority": 8 189 | }, 190 | "27": { 191 | "groups": [ 192 | 9 193 | ], 194 | "name": "Programming languages", 195 | "priority": 5 196 | }, 197 | "28": { 198 | "groups": [ 199 | 7 200 | ], 201 | "name": "Operating systems", 202 | "priority": 6 203 | }, 204 | "29": { 205 | "groups": [ 206 | 3 207 | ], 208 | "name": "Search engines", 209 | "priority": 4 210 | }, 211 | "30": { 212 | "groups": [ 213 | 4 214 | ], 215 | "name": "Webmail", 216 | "priority": 2 217 | }, 218 | "31": { 219 | "groups": [ 220 | 7 221 | ], 222 | "name": "CDN", 223 | "priority": 9 224 | }, 225 | "32": { 226 | "groups": [ 227 | 2 228 | ], 229 | "name": "Marketing automation", 230 | "priority": 9 231 | }, 232 | "33": { 233 | "groups": [ 234 | 7 235 | ], 236 | "name": "Web server extensions", 237 | "priority": 7 238 | }, 239 | "34": { 240 | "groups": [ 241 | 7 242 | ], 243 | "name": "Databases", 244 | "priority": 5 245 | }, 246 | "35": { 247 | "groups": [ 248 | 17 249 | ], 250 | "name": "Maps", 251 | "priority": 6 252 | }, 253 | "36": { 254 | "groups": [ 255 | 2 256 | ], 257 | "name": "Advertising", 258 | "priority": 9 259 | }, 260 | "37": { 261 | "groups": [ 262 | 7 263 | ], 264 | "name": "Network devices", 265 | "priority": 2 266 | }, 267 | "38": { 268 | "groups": [ 269 | 10, 270 | 7 271 | ], 272 | "name": "Media servers", 273 | "priority": 1 274 | }, 275 | "39": { 276 | "groups": [ 277 | 4 278 | ], 279 | "name": "Webcams", 280 | "priority": 9 281 | }, 282 | "41": { 283 | "groups": [ 284 | 1 285 | ], 286 | "name": "Payment processors", 287 | "priority": 8 288 | }, 289 | "42": { 290 | "groups": [ 291 | 8 292 | ], 293 | "name": "Tag managers", 294 | "priority": 9 295 | }, 296 | "44": { 297 | "groups": [ 298 | 9 299 | ], 300 | "name": "CI", 301 | "priority": 3 302 | }, 303 | "45": { 304 | "groups": [ 305 | 7 306 | ], 307 | "name": "Control systems", 308 | "priority": 2 309 | }, 310 | "46": { 311 | "groups": [ 312 | 4 313 | ], 314 | "name": "Remote access", 315 | "priority": 1 316 | }, 317 | "47": { 318 | "groups": [ 319 | 9 320 | ], 321 | "name": "Development", 322 | "priority": 2 323 | }, 324 | "48": { 325 | "groups": [ 326 | 10 327 | ], 328 | "name": "Network storage", 329 | "priority": 2 330 | }, 331 | "49": { 332 | "groups": [ 333 | 3 334 | ], 335 | "name": "Feed readers", 336 | "priority": 1 337 | }, 338 | "50": { 339 | "groups": [ 340 | 3 341 | ], 342 | "name": "DMS", 343 | "priority": 1 344 | }, 345 | "51": { 346 | "groups": [ 347 | 9 348 | ], 349 | "name": "Page builders", 350 | "priority": 1 351 | }, 352 | "52": { 353 | "groups": [ 354 | 4, 355 | 16 356 | ], 357 | "name": "Live chat", 358 | "priority": 9 359 | }, 360 | "53": { 361 | "groups": [ 362 | 2, 363 | 16 364 | ], 365 | "name": "CRM", 366 | "priority": 5 367 | }, 368 | "54": { 369 | "groups": [ 370 | 2 371 | ], 372 | "name": "SEO", 373 | "priority": 8 374 | }, 375 | "55": { 376 | "groups": [ 377 | 16 378 | ], 379 | "name": "Accounting", 380 | "priority": 1 381 | }, 382 | "56": { 383 | "groups": [ 384 | 5 385 | ], 386 | "name": "Cryptominers", 387 | "priority": 5 388 | }, 389 | "57": { 390 | "groups": [ 391 | 9 392 | ], 393 | "name": "Static site generator", 394 | "priority": 1 395 | }, 396 | "58": { 397 | "groups": [ 398 | 6 399 | ], 400 | "name": "User onboarding", 401 | "priority": 8 402 | }, 403 | "59": { 404 | "groups": [ 405 | 9 406 | ], 407 | "name": "JavaScript libraries", 408 | "priority": 9 409 | }, 410 | "60": { 411 | "groups": [ 412 | 7 413 | ], 414 | "name": "Containers", 415 | "priority": 8 416 | }, 417 | "62": { 418 | "groups": [ 419 | 7 420 | ], 421 | "name": "PaaS", 422 | "priority": 8 423 | }, 424 | "63": { 425 | "groups": [ 426 | 7 427 | ], 428 | "name": "IaaS", 429 | "priority": 8 430 | }, 431 | "64": { 432 | "groups": [ 433 | 7 434 | ], 435 | "name": "Reverse proxies", 436 | "priority": 7 437 | }, 438 | "65": { 439 | "groups": [ 440 | 7 441 | ], 442 | "name": "Load balancers", 443 | "priority": 7 444 | }, 445 | "66": { 446 | "groups": [ 447 | 9 448 | ], 449 | "name": "UI frameworks", 450 | "priority": 7 451 | }, 452 | "67": { 453 | "groups": [ 454 | 13 455 | ], 456 | "name": "Cookie compliance", 457 | "priority": 9 458 | }, 459 | "68": { 460 | "groups": [ 461 | 9 462 | ], 463 | "name": "Accessibility", 464 | "priority": 9 465 | }, 466 | "69": { 467 | "groups": [ 468 | 11 469 | ], 470 | "name": "Authentication", 471 | "priority": 6 472 | }, 473 | "70": { 474 | "groups": [ 475 | 11 476 | ], 477 | "name": "SSL/TLS certificate authorities", 478 | "priority": 9 479 | }, 480 | "71": { 481 | "groups": [ 482 | 2 483 | ], 484 | "name": "Affiliate programs", 485 | "priority": 9 486 | }, 487 | "72": { 488 | "groups": [ 489 | 14 490 | ], 491 | "name": "Appointment scheduling", 492 | "priority": 9 493 | }, 494 | "73": { 495 | "groups": [ 496 | 8 497 | ], 498 | "name": "Surveys", 499 | "priority": 9 500 | }, 501 | "74": { 502 | "groups": [ 503 | 8 504 | ], 505 | "name": "A/B Testing", 506 | "priority": 9 507 | }, 508 | "75": { 509 | "groups": [ 510 | 4, 511 | 2 512 | ], 513 | "name": "Email", 514 | "priority": 9 515 | }, 516 | "76": { 517 | "groups": [ 518 | 2 519 | ], 520 | "name": "Personalisation", 521 | "priority": 9 522 | }, 523 | "77": { 524 | "groups": [ 525 | 2 526 | ], 527 | "name": "Retargeting", 528 | "priority": 9 529 | }, 530 | "78": { 531 | "groups": [ 532 | 2 533 | ], 534 | "name": "RUM", 535 | "priority": 9 536 | }, 537 | "79": { 538 | "groups": [ 539 | 17 540 | ], 541 | "name": "Geolocation", 542 | "priority": 9 543 | }, 544 | "80": { 545 | "groups": [ 546 | 15 547 | ], 548 | "name": "WordPress themes", 549 | "priority": 7 550 | }, 551 | "81": { 552 | "groups": [ 553 | 15 554 | ], 555 | "name": "Shopify themes", 556 | "priority": 7 557 | }, 558 | "82": { 559 | "groups": [ 560 | 15 561 | ], 562 | "name": "Drupal themes", 563 | "priority": 7 564 | }, 565 | "83": { 566 | "groups": [ 567 | 8 568 | ], 569 | "name": "Browser fingerprinting", 570 | "priority": 9 571 | }, 572 | "84": { 573 | "groups": [ 574 | 1 575 | ], 576 | "name": "Loyalty & rewards", 577 | "priority": 9 578 | }, 579 | "85": { 580 | "groups": [ 581 | 9 582 | ], 583 | "name": "Feature management", 584 | "priority": 9 585 | }, 586 | "86": { 587 | "groups": [ 588 | 2 589 | ], 590 | "name": "Segmentation", 591 | "priority": 9 592 | }, 593 | "87": { 594 | "groups": [ 595 | 15 596 | ], 597 | "name": "WordPress plugins", 598 | "priority": 8 599 | }, 600 | "88": { 601 | "groups": [ 602 | 7 603 | ], 604 | "name": "Hosting", 605 | "priority": 9 606 | }, 607 | "89": { 608 | "groups": [ 609 | 3 610 | ], 611 | "name": "Translation", 612 | "priority": 9 613 | }, 614 | "90": { 615 | "groups": [ 616 | 2, 617 | 18 618 | ], 619 | "name": "Reviews", 620 | "priority": 9 621 | }, 622 | "91": { 623 | "groups": [ 624 | 1 625 | ], 626 | "name": "Buy now pay later", 627 | "priority": 9 628 | }, 629 | "92": { 630 | "groups": [ 631 | 7 632 | ], 633 | "name": "Performance", 634 | "priority": 9 635 | }, 636 | "93": { 637 | "groups": [ 638 | 14 639 | ], 640 | "name": "Reservations & delivery", 641 | "priority": 9 642 | }, 643 | "94": { 644 | "groups": [ 645 | 2, 646 | 1 647 | ], 648 | "name": "Referral marketing", 649 | "priority": 9 650 | }, 651 | "95": { 652 | "groups": [ 653 | 10 654 | ], 655 | "name": "Digital asset management", 656 | "priority": 9 657 | }, 658 | "96": { 659 | "groups": [ 660 | 2, 661 | 18 662 | ], 663 | "name": "Content curation", 664 | "priority": 9 665 | }, 666 | "97": { 667 | "groups": [ 668 | 2, 669 | 8 670 | ], 671 | "name": "Customer data platform", 672 | "priority": 9 673 | }, 674 | "98": { 675 | "groups": [ 676 | 1 677 | ], 678 | "name": "Cart abandonment", 679 | "priority": 9 680 | }, 681 | "99": { 682 | "groups": [ 683 | 1 684 | ], 685 | "name": "Shipping carriers", 686 | "priority": 9 687 | }, 688 | "100": { 689 | "groups": [ 690 | 15 691 | ], 692 | "name": "Shopify apps", 693 | "priority": 8 694 | }, 695 | "101": { 696 | "groups": [ 697 | 6, 698 | 16 699 | ], 700 | "name": "Recruitment & staffing", 701 | "priority": 9 702 | }, 703 | "102": { 704 | "groups": [ 705 | 1 706 | ], 707 | "name": "Returns", 708 | "priority": 9 709 | }, 710 | "103": { 711 | "groups": [ 712 | 1, 713 | 10 714 | ], 715 | "name": "Livestreaming", 716 | "priority": 9 717 | }, 718 | "104": { 719 | "groups": [ 720 | 14 721 | ], 722 | "name": "Ticket booking", 723 | "priority": 9 724 | }, 725 | "105": { 726 | "groups": [ 727 | 10 728 | ], 729 | "name": "Augmented reality", 730 | "priority": 9 731 | }, 732 | "106": { 733 | "groups": [ 734 | 1 735 | ], 736 | "name": "Cross border ecommerce", 737 | "priority": 6 738 | }, 739 | "107": { 740 | "groups": [ 741 | 1 742 | ], 743 | "name": "Fulfilment", 744 | "priority": 6 745 | }, 746 | "108": { 747 | "groups": [ 748 | 1 749 | ], 750 | "name": "Ecommerce frontends", 751 | "priority": 6 752 | }, 753 | "109": { 754 | "groups": [ 755 | 6 756 | ], 757 | "name": "Domain parking", 758 | "priority": 9 759 | }, 760 | "110": { 761 | "groups": [ 762 | 8 763 | ], 764 | "name": "Form builders", 765 | "priority": 8 766 | }, 767 | "111": { 768 | "groups": [ 769 | 6 770 | ], 771 | "name": "Fundraising & donations", 772 | "priority": 9 773 | } 774 | } -------------------------------------------------------------------------------- /cmd/update-fingerprints/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "flag" 7 | "fmt" 8 | "io" 9 | "log" 10 | "net/http" 11 | "os" 12 | "reflect" 13 | "sort" 14 | "strings" 15 | ) 16 | 17 | var fingerprints = flag.String("fingerprints", "../../fingerprints_data.json", "File to write wappalyzer fingerprints to") 18 | 19 | // Fingerprints contains a map of fingerprints for tech detection 20 | type Fingerprints struct { 21 | // Apps is organized as 22 | Apps map[string]Fingerprint `json:"technologies"` 23 | } 24 | 25 | // Fingerprint is a single piece of information about a tech 26 | type Fingerprint struct { 27 | Cats []int `json:"cats"` 28 | CSS interface{} `json:"css"` 29 | Cookies map[string]string `json:"cookies"` 30 | Dom interface{} `json:"dom"` 31 | JS map[string]string `json:"js"` 32 | Headers map[string]string `json:"headers"` 33 | HTML interface{} `json:"html"` 34 | Script interface{} `json:"scripts"` 35 | ScriptSrc interface{} `json:"scriptSrc"` 36 | Meta map[string]interface{} `json:"meta"` 37 | Implies interface{} `json:"implies"` 38 | Description string `json:"description"` 39 | Website string `json:"website"` 40 | Icon string `json:"icon"` 41 | CPE string `json:"cpe"` 42 | } 43 | 44 | // OutputFingerprints contains a map of fingerprints for tech detection 45 | // optimized and validated for the tech detection package 46 | type OutputFingerprints struct { 47 | // Apps is organized as 48 | Apps map[string]OutputFingerprint `json:"apps"` 49 | } 50 | 51 | // OutputFingerprint is a single piece of information about a tech validated and normalized 52 | type OutputFingerprint struct { 53 | Cats []int `json:"cats,omitempty"` 54 | CSS []string `json:"css,omitempty"` 55 | DOM map[string]map[string]interface{} `json:"dom,omitempty"` 56 | Cookies map[string]string `json:"cookies,omitempty"` 57 | JS map[string]string `json:"js,omitempty"` 58 | Headers map[string]string `json:"headers,omitempty"` 59 | HTML []string `json:"html,omitempty"` 60 | Script []string `json:"scripts,omitempty"` 61 | ScriptSrc []string `json:"scriptSrc,omitempty"` 62 | Meta map[string][]string `json:"meta,omitempty"` 63 | Implies []string `json:"implies,omitempty"` 64 | Description string `json:"description,omitempty"` 65 | Website string `json:"website,omitempty"` 66 | CPE string `json:"cpe,omitempty"` 67 | Icon string `json:"icon,omitempty"` 68 | } 69 | 70 | var fingerprintURLs = []string{ 71 | "https://raw.githubusercontent.com/enthec/webappanalyzer/main/src/technologies/%s.json", 72 | "https://raw.githubusercontent.com/HTTPArchive/wappalyzer/main/src/technologies/%s.json", 73 | } 74 | 75 | func makeFingerprintURLs() []string { 76 | files := []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "_"} 77 | 78 | fingerprints := make([]string, 0, len(files)) 79 | for _, item := range files { 80 | for _, fingerprintURL := range fingerprintURLs { 81 | fingerprints = append(fingerprints, fmt.Sprintf(fingerprintURL, item)) 82 | } 83 | } 84 | return fingerprints 85 | } 86 | 87 | func main() { 88 | flag.Parse() 89 | 90 | fingerprintURLs := makeFingerprintURLs() 91 | 92 | fingerprintsOld := &Fingerprints{ 93 | Apps: make(map[string]Fingerprint), 94 | } 95 | for _, fingerprintItem := range fingerprintURLs { 96 | if err := gatherFingerprintsFromURL(fingerprintItem, fingerprintsOld); err != nil { 97 | log.Fatalf("Could not gather fingerprints %s: %v\n", fingerprintItem, err) 98 | } 99 | } 100 | 101 | log.Printf("Read fingerprints from the server\n") 102 | log.Printf("Starting normalizing of %d fingerprints...\n", len(fingerprintsOld.Apps)) 103 | 104 | outputFingerprints := normalizeFingerprints(fingerprintsOld) 105 | 106 | log.Printf("Got %d valid fingerprints\n", len(outputFingerprints.Apps)) 107 | 108 | fingerprintsFile, err := os.OpenFile(*fingerprints, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o666) 109 | if err != nil { 110 | log.Fatalf("Could not open fingerprints file %s: %s\n", *fingerprints, err) 111 | } 112 | 113 | // sort map keys and pretty print the json to make git diffs useful 114 | 115 | data, err := json.MarshalIndent(outputFingerprints, "", " ") 116 | if err != nil { 117 | log.Fatalf("Could not marshal fingerprints: %s\n", err) 118 | } 119 | _, err = fingerprintsFile.Write(data) 120 | if err != nil { 121 | log.Fatalf("Could not write fingerprints file: %s\n", err) 122 | } 123 | err = fingerprintsFile.Close() 124 | if err != nil { 125 | log.Fatalf("Could not close fingerprints file: %s\n", err) 126 | } 127 | } 128 | 129 | func gatherFingerprintsFromURL(URL string, fingerprints *Fingerprints) error { 130 | req, err := http.NewRequest(http.MethodGet, URL, nil) 131 | if err != nil { 132 | return err 133 | } 134 | 135 | resp, err := http.DefaultClient.Do(req) 136 | if err != nil { 137 | return err 138 | } 139 | defer resp.Body.Close() 140 | 141 | data, err := io.ReadAll(resp.Body) 142 | if err != nil { 143 | return err 144 | } 145 | 146 | fingerprintsOld := &Fingerprints{} 147 | err = json.NewDecoder(bytes.NewReader(data)).Decode(&fingerprintsOld.Apps) 148 | if err != nil { 149 | return err 150 | } 151 | 152 | for k, v := range fingerprintsOld.Apps { 153 | fingerprints.Apps[k] = v 154 | } 155 | return nil 156 | } 157 | 158 | func normalizeFingerprints(fingerprints *Fingerprints) *OutputFingerprints { 159 | outputFingerprints := &OutputFingerprints{Apps: make(map[string]OutputFingerprint)} 160 | 161 | for app, fingerprint := range fingerprints.Apps { 162 | output := OutputFingerprint{ 163 | Cats: fingerprint.Cats, 164 | Cookies: make(map[string]string), 165 | DOM: make(map[string]map[string]interface{}), 166 | Headers: make(map[string]string), 167 | JS: make(map[string]string), 168 | Meta: make(map[string][]string), 169 | Description: fingerprint.Description, 170 | Website: fingerprint.Website, 171 | CPE: fingerprint.CPE, 172 | Icon: fingerprint.Icon, 173 | } 174 | 175 | for cookie, value := range fingerprint.Cookies { 176 | output.Cookies[strings.ToLower(cookie)] = strings.ToLower(value) 177 | } 178 | for k, v := range fingerprint.JS { 179 | output.JS[k] = v 180 | } 181 | 182 | for header, pattern := range fingerprint.Headers { 183 | output.Headers[strings.ToLower(header)] = strings.ToLower(pattern) 184 | } 185 | 186 | // Use reflection for DOM as well 187 | if fingerprint.Dom != nil { 188 | v := reflect.ValueOf(fingerprint.Dom) 189 | 190 | switch v.Kind() { 191 | case reflect.String: 192 | data := v.Interface().(string) 193 | output.DOM[data] = map[string]interface{}{"exists": ""} 194 | case reflect.Slice: 195 | data := v.Interface().([]interface{}) 196 | for _, pattern := range data { 197 | pat := pattern.(string) 198 | output.DOM[pat] = map[string]interface{}{"exists": ""} 199 | } 200 | case reflect.Map: 201 | data := v.Interface().(map[string]interface{}) 202 | for pattern, value := range data { 203 | output.DOM[pattern] = value.(map[string]interface{}) 204 | } 205 | } 206 | } 207 | 208 | // Use reflection type switch for determining HTML tag type 209 | if fingerprint.HTML != nil { 210 | v := reflect.ValueOf(fingerprint.HTML) 211 | 212 | switch v.Kind() { 213 | case reflect.String: 214 | data := v.Interface().(string) 215 | output.HTML = append(output.HTML, strings.ToLower(data)) 216 | case reflect.Slice: 217 | data := v.Interface().([]interface{}) 218 | 219 | for _, pattern := range data { 220 | pat := pattern.(string) 221 | output.HTML = append(output.HTML, strings.ToLower(pat)) 222 | } 223 | } 224 | 225 | sort.Strings(output.HTML) 226 | } 227 | 228 | // Use reflection type switch for determining Script type 229 | if fingerprint.Script != nil { 230 | v := reflect.ValueOf(fingerprint.Script) 231 | 232 | switch v.Kind() { 233 | case reflect.String: 234 | data := v.Interface().(string) 235 | output.Script = append(output.Script, strings.ToLower(data)) 236 | case reflect.Slice: 237 | data := v.Interface().([]interface{}) 238 | for _, pattern := range data { 239 | pat := pattern.(string) 240 | output.Script = append(output.Script, strings.ToLower(pat)) 241 | } 242 | } 243 | 244 | sort.Strings(output.Script) 245 | } 246 | 247 | // Use reflection type switch for determining ScriptSrc type 248 | if fingerprint.ScriptSrc != nil { 249 | v := reflect.ValueOf(fingerprint.ScriptSrc) 250 | 251 | switch v.Kind() { 252 | case reflect.String: 253 | data := v.Interface().(string) 254 | output.ScriptSrc = append(output.ScriptSrc, strings.ToLower(data)) 255 | case reflect.Slice: 256 | data := v.Interface().([]interface{}) 257 | for _, pattern := range data { 258 | pat := pattern.(string) 259 | output.ScriptSrc = append(output.ScriptSrc, strings.ToLower(pat)) 260 | } 261 | } 262 | 263 | sort.Strings(output.ScriptSrc) 264 | } 265 | 266 | for header, pattern := range fingerprint.Meta { 267 | v := reflect.ValueOf(pattern) 268 | 269 | switch v.Kind() { 270 | case reflect.String: 271 | data := strings.ToLower(v.Interface().(string)) 272 | if data == "" { 273 | output.Meta[strings.ToLower(header)] = []string{} 274 | } else { 275 | output.Meta[strings.ToLower(header)] = []string{data} 276 | } 277 | case reflect.Slice: 278 | data := v.Interface().([]interface{}) 279 | 280 | final := []string{} 281 | for _, pattern := range data { 282 | pat := pattern.(string) 283 | final = append(final, strings.ToLower(pat)) 284 | } 285 | sort.Strings(final) 286 | output.Meta[strings.ToLower(header)] = final 287 | } 288 | } 289 | 290 | // Use reflection type switch for determining "Implies" tag type 291 | if fingerprint.Implies != nil { 292 | v := reflect.ValueOf(fingerprint.Implies) 293 | 294 | switch v.Kind() { 295 | case reflect.String: 296 | data := v.Interface().(string) 297 | output.Implies = append(output.Implies, data) 298 | case reflect.Slice: 299 | data := v.Interface().([]interface{}) 300 | for _, pattern := range data { 301 | pat := pattern.(string) 302 | output.Implies = append(output.Implies, pat) 303 | } 304 | } 305 | 306 | sort.Strings(output.Implies) 307 | } 308 | 309 | // Use reflection type switch for determining CSS tag type 310 | if fingerprint.CSS != nil { 311 | v := reflect.ValueOf(fingerprint.CSS) 312 | 313 | switch v.Kind() { 314 | case reflect.String: 315 | data := v.Interface().(string) 316 | output.CSS = append(output.CSS, data) 317 | case reflect.Slice: 318 | data := v.Interface().([]interface{}) 319 | for _, pattern := range data { 320 | pat := pattern.(string) 321 | output.CSS = append(output.CSS, pat) 322 | } 323 | } 324 | 325 | sort.Strings(output.CSS) 326 | } 327 | 328 | // Only add if the fingerprint is valid 329 | outputFingerprints.Apps[app] = output 330 | } 331 | return outputFingerprints 332 | } 333 | -------------------------------------------------------------------------------- /examples/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "log" 7 | "net/http" 8 | 9 | wappalyzer "github.com/projectdiscovery/wappalyzergo" 10 | ) 11 | 12 | func main() { 13 | resp, err := http.DefaultClient.Get("https://www.hackerone.com") 14 | if err != nil { 15 | log.Fatal(err) 16 | } 17 | data, _ := io.ReadAll(resp.Body) // Ignoring error for example 18 | 19 | wappalyzerClient, err := wappalyzer.New() 20 | if err != nil { 21 | log.Fatal(err) 22 | } 23 | fingerprints := wappalyzerClient.Fingerprint(resp.Header, data) 24 | fmt.Printf("%v\n", fingerprints) 25 | // Output: map[Acquia Cloud Platform:{} Amazon EC2:{} Apache:{} Cloudflare:{} Drupal:{} PHP:{} Percona:{} React:{} Varnish:{}] 26 | 27 | fingerprintsWithCats := wappalyzerClient.FingerprintWithCats(resp.Header, data) 28 | fmt.Printf("%v\n", fingerprintsWithCats) 29 | } 30 | -------------------------------------------------------------------------------- /fingerprint_body.go: -------------------------------------------------------------------------------- 1 | package wappalyzer 2 | 3 | import ( 4 | "bytes" 5 | "unsafe" 6 | 7 | "golang.org/x/net/html" 8 | ) 9 | 10 | // checkBody checks for fingerprints in the HTML body 11 | func (s *Wappalyze) checkBody(body []byte) []matchPartResult { 12 | var technologies []matchPartResult 13 | 14 | bodyString := unsafeToString(body) 15 | 16 | technologies = append( 17 | technologies, 18 | s.fingerprints.matchString(bodyString, htmlPart)..., 19 | ) 20 | 21 | // Tokenize the HTML document and check for fingerprints as required 22 | tokenizer := html.NewTokenizer(bytes.NewReader(body)) 23 | 24 | for { 25 | tt := tokenizer.Next() 26 | switch tt { 27 | case html.ErrorToken: 28 | return technologies 29 | case html.StartTagToken: 30 | token := tokenizer.Token() 31 | switch token.Data { 32 | case "script": 33 | // Check if the script tag has a source file to check 34 | source, found := getScriptSource(token) 35 | if found { 36 | // Check the script tags for script fingerprints 37 | technologies = append( 38 | technologies, 39 | s.fingerprints.matchString(source, scriptPart)..., 40 | ) 41 | continue 42 | } 43 | 44 | // Check the text attribute of the tag for javascript based technologies. 45 | // The next token should be the contents of the script tag 46 | if tokenType := tokenizer.Next(); tokenType != html.TextToken { 47 | continue 48 | } 49 | 50 | // TODO: JS requires a running VM, for checking properties. Only 51 | // possible with headless for now :( 52 | 53 | // data := tokenizer.Token().Data 54 | // technologies = append( 55 | // technologies, 56 | // s.fingerprints.matchString(data, jsPart)..., 57 | // ) 58 | case "meta": 59 | // For meta tag, we are only interested in name and content attributes. 60 | name, content, found := getMetaNameAndContent(token) 61 | if !found { 62 | continue 63 | } 64 | technologies = append( 65 | technologies, 66 | s.fingerprints.matchKeyValueString(name, content, metaPart)..., 67 | ) 68 | } 69 | case html.SelfClosingTagToken: 70 | token := tokenizer.Token() 71 | if token.Data != "meta" { 72 | continue 73 | } 74 | 75 | // Parse the meta tag and check for tech 76 | name, content, found := getMetaNameAndContent(token) 77 | if !found { 78 | continue 79 | } 80 | technologies = append( 81 | technologies, 82 | s.fingerprints.matchKeyValueString(name, content, metaPart)..., 83 | ) 84 | } 85 | } 86 | } 87 | 88 | func (s *Wappalyze) getTitle(body []byte) string { 89 | var title string 90 | 91 | // Tokenize the HTML document and check for fingerprints as required 92 | tokenizer := html.NewTokenizer(bytes.NewReader(body)) 93 | 94 | for { 95 | tt := tokenizer.Next() 96 | switch tt { 97 | case html.ErrorToken: 98 | return title 99 | case html.StartTagToken: 100 | token := tokenizer.Token() 101 | switch token.Data { 102 | case "title": 103 | // Next text token will be the actual title of the page 104 | if tokenType := tokenizer.Next(); tokenType != html.TextToken { 105 | continue 106 | } 107 | title = tokenizer.Token().Data 108 | } 109 | } 110 | } 111 | } 112 | 113 | // getMetaNameAndContent gets name and content attributes from meta html token 114 | func getMetaNameAndContent(token html.Token) (string, string, bool) { 115 | if len(token.Attr) < keyValuePairLength { 116 | return "", "", false 117 | } 118 | 119 | var name, content string 120 | for _, attr := range token.Attr { 121 | switch attr.Key { 122 | case "name": 123 | name = attr.Val 124 | case "content": 125 | content = attr.Val 126 | } 127 | } 128 | return name, content, true 129 | } 130 | 131 | // getScriptSource gets src tag from a script tag 132 | func getScriptSource(token html.Token) (string, bool) { 133 | if len(token.Attr) < 1 { 134 | return "", false 135 | } 136 | 137 | var source string 138 | for _, attr := range token.Attr { 139 | switch attr.Key { 140 | case "src": 141 | source = attr.Val 142 | } 143 | } 144 | return source, true 145 | } 146 | 147 | // unsafeToString converts a byte slice to string and does it with 148 | // zero allocations. 149 | // 150 | // NOTE: This function should only be used if its certain that the underlying 151 | // array has not been manipulated. 152 | // 153 | // Reference - https://github.com/golang/go/issues/25484 154 | func unsafeToString(data []byte) string { 155 | return *(*string)(unsafe.Pointer(&data)) 156 | } 157 | -------------------------------------------------------------------------------- /fingerprint_cookies.go: -------------------------------------------------------------------------------- 1 | package wappalyzer 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | // checkCookies checks if the cookies for a target match the fingerprints 8 | // and returns the matched IDs if any. 9 | func (s *Wappalyze) checkCookies(cookies []string) []matchPartResult { 10 | // Normalize the cookies for further processing 11 | normalized := s.normalizeCookies(cookies) 12 | 13 | technologies := s.fingerprints.matchMapString(normalized, cookiesPart) 14 | return technologies 15 | } 16 | 17 | const keyValuePairLength = 2 18 | 19 | // normalizeCookies normalizes the cookies and returns an 20 | // easily parsed format that can be processed upon. 21 | func (s *Wappalyze) normalizeCookies(cookies []string) map[string]string { 22 | normalized := make(map[string]string) 23 | 24 | for _, part := range cookies { 25 | parts := strings.SplitN(strings.Trim(part, " "), "=", keyValuePairLength) 26 | if len(parts) < keyValuePairLength { 27 | continue 28 | } 29 | normalized[parts[0]] = parts[1] 30 | } 31 | return normalized 32 | } 33 | 34 | // findSetCookie finds the set cookie header from the normalized headers 35 | func (s *Wappalyze) findSetCookie(headers map[string]string) []string { 36 | value, ok := headers["set-cookie"] 37 | if !ok { 38 | return nil 39 | } 40 | 41 | var values []string 42 | for _, v := range strings.Split(value, " ") { 43 | if v == "" { 44 | continue 45 | } 46 | if strings.Contains(v, ",") { 47 | values = append(values, strings.Split(v, ",")...) 48 | } else if strings.Contains(v, ";") { 49 | values = append(values, strings.Split(v, ";")...) 50 | } else { 51 | values = append(values, v) 52 | } 53 | } 54 | return values 55 | } 56 | -------------------------------------------------------------------------------- /fingerprint_headers.go: -------------------------------------------------------------------------------- 1 | package wappalyzer 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | // checkHeaders checks if the headers for a target match the fingerprints 8 | // and returns the matched IDs if any. 9 | func (s *Wappalyze) checkHeaders(headers map[string]string) []matchPartResult { 10 | technologies := s.fingerprints.matchMapString(headers, headersPart) 11 | return technologies 12 | } 13 | 14 | // normalizeHeaders normalizes the headers for the tech discovery on headers 15 | func (s *Wappalyze) normalizeHeaders(headers map[string][]string) map[string]string { 16 | normalized := make(map[string]string, len(headers)) 17 | data := getHeadersMap(headers) 18 | 19 | for header, value := range data { 20 | normalized[strings.ToLower(header)] = strings.ToLower(value) 21 | } 22 | return normalized 23 | } 24 | 25 | // GetHeadersMap returns a map[string]string of response headers 26 | func getHeadersMap(headersArray map[string][]string) map[string]string { 27 | headers := make(map[string]string, len(headersArray)) 28 | 29 | builder := &strings.Builder{} 30 | for key, value := range headersArray { 31 | for i, v := range value { 32 | builder.WriteString(v) 33 | if i != len(value)-1 { 34 | builder.WriteString(", ") 35 | } 36 | } 37 | headerValue := builder.String() 38 | 39 | headers[key] = headerValue 40 | builder.Reset() 41 | } 42 | return headers 43 | } 44 | -------------------------------------------------------------------------------- /fingerprints.go: -------------------------------------------------------------------------------- 1 | package wappalyzer 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // Fingerprints contains a map of fingerprints for tech detection 8 | type Fingerprints struct { 9 | // Apps is organized as 10 | Apps map[string]*Fingerprint `json:"apps"` 11 | } 12 | 13 | // Fingerprint is a single piece of information about a tech validated and normalized 14 | type Fingerprint struct { 15 | Cats []int `json:"cats"` 16 | CSS []string `json:"css"` 17 | Cookies map[string]string `json:"cookies"` 18 | Dom map[string]map[string]interface{} `json:"dom"` 19 | JS map[string]string `json:"js"` 20 | Headers map[string]string `json:"headers"` 21 | HTML []string `json:"html"` 22 | Script []string `json:"scripts"` 23 | ScriptSrc []string `json:"scriptSrc"` 24 | Meta map[string][]string `json:"meta"` 25 | Implies []string `json:"implies"` 26 | Description string `json:"description"` 27 | Website string `json:"website"` 28 | CPE string `json:"cpe"` 29 | Icon string `json:"icon"` 30 | } 31 | 32 | // CompiledFingerprints contains a map of fingerprints for tech detection 33 | type CompiledFingerprints struct { 34 | // Apps is organized as 35 | Apps map[string]*CompiledFingerprint 36 | } 37 | 38 | // CompiledFingerprint contains the compiled fingerprints from the tech json 39 | type CompiledFingerprint struct { 40 | // cats contain categories that are implicit with this tech 41 | cats []int 42 | // implies contains technologies that are implicit with this tech 43 | implies []string 44 | // description contains fingerprint description 45 | description string 46 | // website contains a URL associated with the fingerprint 47 | website string 48 | // icon contains a Icon associated with the fingerprint 49 | icon string 50 | // cookies contains fingerprints for target cookies 51 | cookies map[string]*ParsedPattern 52 | // js contains fingerprints for the js file 53 | js map[string]*ParsedPattern 54 | // dom contains fingerprints for the target dom 55 | dom map[string]map[string]*ParsedPattern 56 | // headers contains fingerprints for target headers 57 | headers map[string]*ParsedPattern 58 | // html contains fingerprints for the target HTML 59 | html []*ParsedPattern 60 | // script contains fingerprints for scripts 61 | script []*ParsedPattern 62 | // scriptSrc contains fingerprints for script srcs 63 | scriptSrc []*ParsedPattern 64 | // meta contains fingerprints for meta tags 65 | meta map[string][]*ParsedPattern 66 | // cpe contains the cpe for a fingerpritn 67 | cpe string 68 | } 69 | 70 | func (f *CompiledFingerprint) GetJSRules() map[string]*ParsedPattern { 71 | return f.js 72 | } 73 | 74 | func (f *CompiledFingerprint) GetDOMRules() map[string]map[string]*ParsedPattern { 75 | return f.dom 76 | } 77 | 78 | // AppInfo contains basic information about an App. 79 | type AppInfo struct { 80 | Description string 81 | Website string 82 | CPE string 83 | Icon string 84 | Categories []string 85 | } 86 | 87 | // CatsInfo contains basic information about an App. 88 | type CatsInfo struct { 89 | Cats []int 90 | } 91 | 92 | // part is the part of the fingerprint to match 93 | type part int 94 | 95 | // parts that can be matched 96 | const ( 97 | cookiesPart part = iota + 1 98 | jsPart 99 | headersPart 100 | htmlPart 101 | scriptPart 102 | metaPart 103 | ) 104 | 105 | // loadPatterns loads the fingerprint patterns and compiles regexes 106 | func compileFingerprint(fingerprint *Fingerprint) *CompiledFingerprint { 107 | compiled := &CompiledFingerprint{ 108 | cats: fingerprint.Cats, 109 | implies: fingerprint.Implies, 110 | description: fingerprint.Description, 111 | website: fingerprint.Website, 112 | icon: fingerprint.Icon, 113 | dom: make(map[string]map[string]*ParsedPattern), 114 | cookies: make(map[string]*ParsedPattern), 115 | js: make(map[string]*ParsedPattern), 116 | headers: make(map[string]*ParsedPattern), 117 | html: make([]*ParsedPattern, 0, len(fingerprint.HTML)), 118 | script: make([]*ParsedPattern, 0, len(fingerprint.Script)), 119 | scriptSrc: make([]*ParsedPattern, 0, len(fingerprint.ScriptSrc)), 120 | meta: make(map[string][]*ParsedPattern), 121 | cpe: fingerprint.CPE, 122 | } 123 | 124 | for dom, patterns := range fingerprint.Dom { 125 | compiled.dom[dom] = make(map[string]*ParsedPattern) 126 | 127 | for attr, value := range patterns { 128 | switch attr { 129 | case "exists", "text": 130 | pattern, err := ParsePattern(value.(string)) 131 | if err != nil { 132 | continue 133 | } 134 | compiled.dom[dom]["main"] = pattern 135 | case "attributes": 136 | attrMap, ok := value.(map[string]interface{}) 137 | if !ok { 138 | continue 139 | } 140 | compiled.dom[dom] = make(map[string]*ParsedPattern) 141 | for attrName, value := range attrMap { 142 | pattern, err := ParsePattern(value.(string)) 143 | if err != nil { 144 | continue 145 | } 146 | compiled.dom[dom][attrName] = pattern 147 | } 148 | } 149 | } 150 | } 151 | 152 | for header, pattern := range fingerprint.Cookies { 153 | fingerprint, err := ParsePattern(pattern) 154 | if err != nil { 155 | continue 156 | } 157 | compiled.cookies[header] = fingerprint 158 | } 159 | 160 | for k, pattern := range fingerprint.JS { 161 | fingerprint, err := ParsePattern(pattern) 162 | if err != nil { 163 | continue 164 | } 165 | compiled.js[k] = fingerprint 166 | } 167 | 168 | for header, pattern := range fingerprint.Headers { 169 | fingerprint, err := ParsePattern(pattern) 170 | if err != nil { 171 | continue 172 | } 173 | compiled.headers[header] = fingerprint 174 | } 175 | 176 | for _, pattern := range fingerprint.HTML { 177 | fingerprint, err := ParsePattern(pattern) 178 | if err != nil { 179 | continue 180 | } 181 | compiled.html = append(compiled.html, fingerprint) 182 | } 183 | 184 | for _, pattern := range fingerprint.Script { 185 | fingerprint, err := ParsePattern(pattern) 186 | if err != nil { 187 | continue 188 | } 189 | compiled.script = append(compiled.script, fingerprint) 190 | } 191 | 192 | for _, pattern := range fingerprint.ScriptSrc { 193 | fingerprint, err := ParsePattern(pattern) 194 | if err != nil { 195 | continue 196 | } 197 | compiled.scriptSrc = append(compiled.scriptSrc, fingerprint) 198 | } 199 | 200 | for meta, patterns := range fingerprint.Meta { 201 | var compiledList []*ParsedPattern 202 | 203 | for _, pattern := range patterns { 204 | fingerprint, err := ParsePattern(pattern) 205 | if err != nil { 206 | continue 207 | } 208 | compiledList = append(compiledList, fingerprint) 209 | } 210 | compiled.meta[meta] = compiledList 211 | } 212 | return compiled 213 | } 214 | 215 | // matchString matches a string for the fingerprints 216 | func (f *CompiledFingerprints) matchString(data string, part part) []matchPartResult { 217 | var matched bool 218 | var technologies []matchPartResult 219 | 220 | for app, fingerprint := range f.Apps { 221 | var version string 222 | confidence := 100 223 | 224 | switch part { 225 | case jsPart: 226 | for _, pattern := range fingerprint.js { 227 | if valid, versionString := pattern.Evaluate(data); valid { 228 | matched = true 229 | if version == "" && versionString != "" { 230 | version = versionString 231 | } 232 | confidence = pattern.Confidence 233 | } 234 | } 235 | case scriptPart: 236 | for _, pattern := range fingerprint.scriptSrc { 237 | if valid, versionString := pattern.Evaluate(data); valid { 238 | matched = true 239 | if version == "" && versionString != "" { 240 | version = versionString 241 | } 242 | confidence = pattern.Confidence 243 | } 244 | } 245 | case htmlPart: 246 | for _, pattern := range fingerprint.html { 247 | if valid, versionString := pattern.Evaluate(data); valid { 248 | matched = true 249 | if version == "" && versionString != "" { 250 | version = versionString 251 | } 252 | confidence = pattern.Confidence 253 | } 254 | } 255 | } 256 | 257 | // If no match, continue with the next fingerprint 258 | if !matched { 259 | continue 260 | } 261 | 262 | // Append the technologies as well as implied ones 263 | technologies = append(technologies, matchPartResult{ 264 | application: app, 265 | version: version, 266 | confidence: confidence, 267 | }) 268 | if len(fingerprint.implies) > 0 { 269 | for _, implies := range fingerprint.implies { 270 | technologies = append(technologies, matchPartResult{ 271 | application: implies, 272 | confidence: confidence, 273 | }) 274 | } 275 | } 276 | matched = false 277 | } 278 | return technologies 279 | } 280 | 281 | // matchKeyValue matches a key-value store map for the fingerprints 282 | func (f *CompiledFingerprints) matchKeyValueString(key, value string, part part) []matchPartResult { 283 | var matched bool 284 | var technologies []matchPartResult 285 | 286 | for app, fingerprint := range f.Apps { 287 | var version string 288 | confidence := 100 289 | 290 | switch part { 291 | case cookiesPart: 292 | for data, pattern := range fingerprint.cookies { 293 | if data != key { 294 | continue 295 | } 296 | 297 | if valid, versionString := pattern.Evaluate(value); valid { 298 | matched = true 299 | if version == "" && versionString != "" { 300 | version = versionString 301 | } 302 | confidence = pattern.Confidence 303 | break 304 | } 305 | } 306 | case headersPart: 307 | for data, pattern := range fingerprint.headers { 308 | if data != key { 309 | continue 310 | } 311 | 312 | if valid, versionString := pattern.Evaluate(value); valid { 313 | matched = true 314 | if version == "" && versionString != "" { 315 | version = versionString 316 | } 317 | confidence = pattern.Confidence 318 | break 319 | } 320 | } 321 | case metaPart: 322 | for data, patterns := range fingerprint.meta { 323 | if data != key { 324 | continue 325 | } 326 | 327 | for _, pattern := range patterns { 328 | if valid, versionString := pattern.Evaluate(value); valid { 329 | matched = true 330 | if version == "" && versionString != "" { 331 | version = versionString 332 | } 333 | confidence = pattern.Confidence 334 | break 335 | } 336 | } 337 | } 338 | } 339 | 340 | // If no match, continue with the next fingerprint 341 | if !matched { 342 | continue 343 | } 344 | 345 | technologies = append(technologies, matchPartResult{ 346 | application: app, 347 | version: version, 348 | confidence: confidence, 349 | }) 350 | if len(fingerprint.implies) > 0 { 351 | for _, implies := range fingerprint.implies { 352 | technologies = append(technologies, matchPartResult{ 353 | application: implies, 354 | confidence: confidence, 355 | }) 356 | } 357 | } 358 | matched = false 359 | } 360 | return technologies 361 | } 362 | 363 | // matchMapString matches a key-value store map for the fingerprints 364 | func (f *CompiledFingerprints) matchMapString(keyValue map[string]string, part part) []matchPartResult { 365 | var matched bool 366 | var technologies []matchPartResult 367 | 368 | for app, fingerprint := range f.Apps { 369 | var version string 370 | confidence := 100 371 | 372 | switch part { 373 | case cookiesPart: 374 | for data, pattern := range fingerprint.cookies { 375 | value, ok := keyValue[data] 376 | if !ok { 377 | continue 378 | } 379 | if pattern == nil { 380 | matched = true 381 | } 382 | if valid, versionString := pattern.Evaluate(value); valid { 383 | matched = true 384 | if version == "" && versionString != "" { 385 | version = versionString 386 | } 387 | confidence = pattern.Confidence 388 | break 389 | } 390 | } 391 | case headersPart: 392 | for data, pattern := range fingerprint.headers { 393 | value, ok := keyValue[data] 394 | if !ok { 395 | continue 396 | } 397 | 398 | if valid, versionString := pattern.Evaluate(value); valid { 399 | matched = true 400 | if version == "" && versionString != "" { 401 | version = versionString 402 | } 403 | confidence = pattern.Confidence 404 | break 405 | } 406 | } 407 | case metaPart: 408 | for data, patterns := range fingerprint.meta { 409 | value, ok := keyValue[data] 410 | if !ok { 411 | continue 412 | } 413 | 414 | for _, pattern := range patterns { 415 | if valid, versionString := pattern.Evaluate(value); valid { 416 | matched = true 417 | if version == "" && versionString != "" { 418 | version = versionString 419 | } 420 | confidence = pattern.Confidence 421 | break 422 | } 423 | } 424 | } 425 | } 426 | 427 | // If no match, continue with the next fingerprint 428 | if !matched { 429 | continue 430 | } 431 | 432 | technologies = append(technologies, matchPartResult{ 433 | application: app, 434 | version: version, 435 | confidence: confidence, 436 | }) 437 | if len(fingerprint.implies) > 0 { 438 | for _, implies := range fingerprint.implies { 439 | technologies = append(technologies, matchPartResult{ 440 | application: implies, 441 | confidence: confidence, 442 | }) 443 | } 444 | } 445 | matched = false 446 | } 447 | return technologies 448 | } 449 | 450 | func FormatAppVersion(app, version string) string { 451 | if version == "" { 452 | return app 453 | } 454 | return fmt.Sprintf("%s:%s", app, version) 455 | } 456 | 457 | // GetFingerprints returns the fingerprint string from wappalyzer 458 | func GetFingerprints() string { 459 | return fingerprints 460 | } 461 | -------------------------------------------------------------------------------- /fingerprints_data.go: -------------------------------------------------------------------------------- 1 | package wappalyzer 2 | 3 | import ( 4 | _ "embed" 5 | "encoding/json" 6 | "strconv" 7 | "sync" 8 | ) 9 | 10 | var ( 11 | //go:embed fingerprints_data.json 12 | fingerprints string 13 | //go:embed categories_data.json 14 | cateogriesData string 15 | 16 | syncOnce sync.Once 17 | categoriesMapping map[int]categoryItem 18 | ) 19 | 20 | func init() { 21 | syncOnce.Do(func() { 22 | categoriesMapping = make(map[int]categoryItem) 23 | 24 | var categories map[string]categoryItem 25 | if err := json.Unmarshal([]byte(cateogriesData), &categories); err != nil { 26 | panic(err) 27 | } 28 | for category, data := range categories { 29 | parsed, _ := strconv.Atoi(category) 30 | categoriesMapping[parsed] = data 31 | } 32 | }) 33 | } 34 | 35 | func GetRawFingerprints() string { 36 | return fingerprints 37 | } 38 | 39 | func GetCategoriesMapping() map[int]categoryItem { 40 | return categoriesMapping 41 | } 42 | 43 | type categoryItem struct { 44 | Name string `json:"name"` 45 | Priority int `json:"priority"` 46 | } 47 | -------------------------------------------------------------------------------- /fingerprints_test.go: -------------------------------------------------------------------------------- 1 | package wappalyzer 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func Test_All_Match_Paths(t *testing.T) { 10 | wappalyzer, err := New() 11 | require.NoError(t, err, "could not create wappalyzer") 12 | 13 | matches := wappalyzer.Fingerprint(map[string][]string{ 14 | "Server": {"Apache/2.4.29"}, 15 | }, []byte("")) 16 | require.NotNil(t, matches, "could not get matches") 17 | require.Equal(t, map[string]struct{}{"Apache HTTP Server:2.4.29": {}}, matches, "could not match apache") 18 | } 19 | 20 | func Test_New_Panicking_Regex(t *testing.T) { 21 | matcher, err := ParsePattern("([\\d\\.]+)\\;version:\\1\\;confidence:0") 22 | require.NotNil(t, matcher, "could create invalid version regex") 23 | require.NoError(t, err, "could create invalid version regex") 24 | } 25 | 26 | func TestVersionRegex(t *testing.T) { 27 | regex, err := ParsePattern("JBoss(?:-([\\d.]+))?\\;confidence:50\\;version:\\1") 28 | require.NoError(t, err, "could not create version regex") 29 | 30 | matched, version := regex.Evaluate("JBoss-2.3.9") 31 | require.True(t, matched, "could not get version regex match") 32 | require.Equal(t, "2.3.9", version, "could not get correct version") 33 | 34 | t.Run("confidence-only", func(t *testing.T) { 35 | _, err := ParsePattern("\\;confidence:50") 36 | require.NoError(t, err, "could create invalid version regex") 37 | }) 38 | 39 | t.Run("blank", func(t *testing.T) { 40 | matcher, err := ParsePattern("") 41 | require.NoError(t, err, "could create invalid version regex") 42 | 43 | matched, _ := matcher.Evaluate("JBoss-2.3.9") 44 | require.True(t, matched, "should match anything") 45 | }) 46 | } 47 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/projectdiscovery/wappalyzergo 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/stretchr/testify v1.10.0 7 | golang.org/x/net v0.40.0 8 | ) 9 | 10 | require ( 11 | github.com/davecgh/go-spew v1.1.1 // indirect 12 | github.com/pmezard/go-difflib v1.0.0 // indirect 13 | gopkg.in/yaml.v3 v3.0.1 // indirect 14 | ) 15 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 6 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 7 | golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= 8 | golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= 9 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 10 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 11 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 12 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 13 | -------------------------------------------------------------------------------- /patterns.go: -------------------------------------------------------------------------------- 1 | package wappalyzer 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | // ParsedPattern encapsulates a regular expression with 11 | // additional metadata for confidence and version extraction. 12 | type ParsedPattern struct { 13 | regex *regexp.Regexp 14 | 15 | Confidence int 16 | Version string 17 | SkipRegex bool 18 | } 19 | 20 | const ( 21 | verCap1 = `(\d+(?:\.\d+)+)` // captures 1 set of digits '\d+' followed by one or more '\.\d+' patterns 22 | verCap1Fill = "__verCap1__" 23 | verCap1Limited = `(\d{1,20}(?:\.\d{1,20}){1,20})` 24 | 25 | verCap2 = `((?:\d+\.)+\d+)` // captures 1 or more '\d+\.' patterns followed by 1 set of digits '\d+' 26 | verCap2Fill = "__verCap2__" 27 | verCap2Limited = `((?:\d{1,20}\.){1,20}\d{1,20})` 28 | ) 29 | 30 | // ParsePattern extracts information from a pattern, supporting both regex and simple patterns 31 | func ParsePattern(pattern string) (*ParsedPattern, error) { 32 | parts := strings.Split(pattern, "\\;") 33 | p := &ParsedPattern{Confidence: 100} 34 | 35 | if parts[0] == "" { 36 | p.SkipRegex = true 37 | } 38 | for i, part := range parts { 39 | if i == 0 { 40 | if p.SkipRegex { 41 | continue 42 | } 43 | regexPattern := part 44 | 45 | // save version capture groups 46 | regexPattern = strings.ReplaceAll(regexPattern, verCap1, verCap1Fill) 47 | regexPattern = strings.ReplaceAll(regexPattern, verCap2, verCap2Fill) 48 | 49 | regexPattern = strings.ReplaceAll(regexPattern, "\\+", "__escapedPlus__") 50 | regexPattern = strings.ReplaceAll(regexPattern, "+", "{1,250}") 51 | regexPattern = strings.ReplaceAll(regexPattern, "*", "{0,250}") 52 | regexPattern = strings.ReplaceAll(regexPattern, "__escapedPlus__", "\\+") 53 | 54 | // restore version capture groups 55 | regexPattern = strings.ReplaceAll(regexPattern, verCap1Fill, verCap1Limited) 56 | regexPattern = strings.ReplaceAll(regexPattern, verCap2Fill, verCap2Limited) 57 | 58 | var err error 59 | p.regex, err = regexp.Compile("(?i)" + regexPattern) 60 | if err != nil { 61 | return nil, err 62 | } 63 | } else { 64 | keyValue := strings.SplitN(part, ":", 2) 65 | if len(keyValue) < 2 { 66 | continue 67 | } 68 | 69 | switch keyValue[0] { 70 | case "confidence": 71 | conf, err := strconv.Atoi(keyValue[1]) 72 | if err != nil { 73 | // If conversion fails, keep default confidence 74 | p.Confidence = 100 75 | } else { 76 | p.Confidence = conf 77 | } 78 | case "version": 79 | p.Version = keyValue[1] 80 | } 81 | } 82 | } 83 | return p, nil 84 | } 85 | 86 | func (p *ParsedPattern) Evaluate(target string) (bool, string) { 87 | if p.SkipRegex { 88 | return true, "" 89 | } 90 | if p.regex == nil { 91 | return false, "" 92 | } 93 | 94 | submatches := p.regex.FindStringSubmatch(target) 95 | if len(submatches) == 0 { 96 | return false, "" 97 | } 98 | extractedVersion, _ := p.extractVersion(submatches) 99 | return true, extractedVersion 100 | } 101 | 102 | // extractVersion uses the provided pattern to extract version information from a target string. 103 | func (p *ParsedPattern) extractVersion(submatches []string) (string, error) { 104 | if len(submatches) == 0 { 105 | return "", nil // No matches found 106 | } 107 | 108 | result := p.Version 109 | for i, match := range submatches[1:] { // Start from 1 to skip the entire match 110 | placeholder := fmt.Sprintf("\\%d", i+1) 111 | result = strings.ReplaceAll(result, placeholder, match) 112 | } 113 | 114 | // Evaluate any ternary expressions in the result 115 | result, err := evaluateVersionExpression(result, submatches[1:]) 116 | if err != nil { 117 | return "", err 118 | } 119 | return strings.TrimSpace(result), nil 120 | } 121 | 122 | // evaluateVersionExpression handles ternary expressions in version strings. 123 | func evaluateVersionExpression(expression string, submatches []string) (string, error) { 124 | if strings.Contains(expression, "?") { 125 | parts := strings.Split(expression, "?") 126 | if len(parts) != 2 { 127 | return "", fmt.Errorf("invalid ternary expression: %s", expression) 128 | } 129 | 130 | trueFalseParts := strings.Split(parts[1], ":") 131 | if len(trueFalseParts) != 2 { 132 | return "", fmt.Errorf("invalid true/false parts in ternary expression: %s", expression) 133 | } 134 | 135 | if trueFalseParts[0] != "" { // Simple existence check 136 | if len(submatches) == 0 { 137 | return trueFalseParts[1], nil 138 | } 139 | return trueFalseParts[0], nil 140 | } 141 | if trueFalseParts[1] == "" { 142 | if len(submatches) == 0 { 143 | return "", nil 144 | } 145 | return trueFalseParts[0], nil 146 | } 147 | return trueFalseParts[1], nil 148 | } 149 | 150 | return expression, nil 151 | } 152 | -------------------------------------------------------------------------------- /patterns_test.go: -------------------------------------------------------------------------------- 1 | package wappalyzer 2 | 3 | import "testing" 4 | 5 | func TestParsePattern(t *testing.T) { 6 | tests := []struct { 7 | name string 8 | input string 9 | expectedRegex string 10 | expectedConf int 11 | expectedVer string 12 | expectError bool 13 | }{ 14 | { 15 | name: "Basic pattern", 16 | input: "Mage.*", 17 | expectedRegex: "(?i)Mage.{0,250}", 18 | expectedConf: 100, 19 | }, 20 | { 21 | name: "With confidence", 22 | input: "Mage.*\\;confidence:50", 23 | expectedRegex: "(?i)Mage.{0,250}", 24 | expectedConf: 50, 25 | }, 26 | { 27 | name: "With version", 28 | input: "jquery-([0-9.]+)\\.js\\;version:\\1", 29 | expectedRegex: "(?i)jquery-([0-9.]{1,250})\\.js", 30 | expectedConf: 100, 31 | expectedVer: "\\1", 32 | }, 33 | { 34 | name: "Complex pattern - 1", 35 | input: "/wp-content/themes/make(?:-child)?/.+frontend\\.js(?:\\?ver=(\\d+(?:\\.\\d+)+))?\\;version:\\1", 36 | expectedRegex: `(?i)/wp-content/themes/make(?:-child)?/.{1,250}frontend\.js(?:\?ver=(\d{1,20}(?:\.\d{1,20}){1,20}))?`, 37 | expectedConf: 100, 38 | expectedVer: "\\1", 39 | }, 40 | { 41 | name: "Complex pattern - 2", 42 | input: "(?:((?:\\d+\\.)+\\d+)\\/)?chroma(?:\\.min)?\\.js\\;version:\\1", 43 | expectedRegex: `(?i)(?:((?:\d{1,20}\.){1,20}\d{1,20})\/)?chroma(?:\.min)?\.js`, 44 | expectedConf: 100, 45 | expectedVer: "\\1", 46 | }, 47 | { 48 | name: "Complex pattern - 3", 49 | input: "(?:((?:\\d+\\.)+\\d+)\\/(?:dc\\/)?)?dc(?:\\.leaflet)?\\.js\\;version:\\1", 50 | expectedRegex: `(?i)(?:((?:\d{1,20}\.){1,20}\d{1,20})\/(?:dc\/)?)?dc(?:\.leaflet)?\.js`, 51 | expectedConf: 100, 52 | expectedVer: "\\1", 53 | }, 54 | } 55 | 56 | for _, tt := range tests { 57 | t.Run(tt.name, func(t *testing.T) { 58 | pattern, err := ParsePattern(tt.input) 59 | if (err != nil) != tt.expectError { 60 | t.Errorf("parsePattern() error = %v, expectError %v", err, tt.expectError) 61 | return 62 | } 63 | if err == nil { 64 | if pattern.regex.String() != tt.expectedRegex { 65 | t.Errorf("Expected regex = %s, got %s", tt.expectedRegex, pattern.regex.String()) 66 | } 67 | if pattern.Confidence != tt.expectedConf { 68 | t.Errorf("Expected confidence = %d, got %d", tt.expectedConf, pattern.Confidence) 69 | } 70 | if pattern.Version != tt.expectedVer { 71 | t.Errorf("Expected version = %s, got %s", tt.expectedVer, pattern.Version) 72 | } 73 | } 74 | }) 75 | } 76 | } 77 | func TestExtractVersion(t *testing.T) { 78 | tests := []struct { 79 | name string 80 | pattern string 81 | target string 82 | expectedVer string 83 | expectError bool 84 | }{ 85 | { 86 | name: "Simple version extraction", 87 | pattern: "Mage ([0-9.]+)\\;version:\\1", 88 | target: "Mage 2.3", 89 | expectedVer: "2.3", 90 | expectError: false, 91 | }, 92 | { 93 | name: "Version with ternary - true", 94 | pattern: "Mage ([0-9.]+)\\;version:\\1?found:", 95 | target: "Mage 2.3", 96 | expectedVer: "found", 97 | expectError: false, 98 | }, 99 | { 100 | name: "Version with ternary - false", 101 | pattern: "Mage\\;version:\\1?:not found", 102 | target: "Mage", 103 | expectedVer: "not found", 104 | expectError: false, 105 | }, 106 | { 107 | name: "First version pattern", 108 | pattern: "Mage ([0-9.]+)\\;version:\\1?a:", 109 | target: "Mage 2.3", 110 | expectedVer: "a", 111 | expectError: false, 112 | }, 113 | { 114 | name: "Complex pattern", 115 | pattern: "([\\d.]+)?/modernizr(?:\\.([\\d.]+))?.*\\.js\\;version:\\1?\\1:\\2", 116 | target: "2.6.2/modernizr.js", 117 | expectedVer: "2.6.2", 118 | expectError: false, 119 | }, 120 | { 121 | name: "Complex pattern - 2", 122 | pattern: "([\\d.]+)?/modernizr(?:\\.([\\d.]+))?.*\\.js\\;version:\\1?\\1:\\2", 123 | target: "/modernizr.2.5.7.js", 124 | expectedVer: "2.5.7", 125 | expectError: false, 126 | }, 127 | { 128 | name: "Complex pattern - 3", 129 | pattern: "(?:apache(?:$|/([\\d.]+)|[^/-])|(?:^|\\b)httpd)\\;version:\\1", 130 | target: "apache", 131 | expectError: false, 132 | }, 133 | { 134 | name: "Complex pattern - 4", 135 | pattern: "(?:apache(?:$|/([\\d.]+)|[^/-])|(?:^|\\b)httpd)\\;version:\\1", 136 | target: "apache/2.4.29", 137 | expectedVer: "2.4.29", 138 | expectError: false, 139 | }, 140 | { 141 | name: "Complex pattern - 5", 142 | pattern: "/wp-content/themes/make(?:-child)?/.+frontend\\.js(?:\\?ver=(\\d+(?:\\.\\d+)+))?\\;version:\\1", 143 | target: "/wp-content/themes/make-child/whatever/frontend.js?ver=1.9.1", 144 | expectedVer: "1.9.1", 145 | expectError: false, 146 | }, 147 | { 148 | name: "Complex pattern - 6", 149 | pattern: "(?:((?:\\d+\\.)+\\d+)\\/)?chroma(?:\\.min)?\\.js\\;version:\\1", 150 | target: "/ajax/libs/chroma-js/2.4.2/chroma.min.js", 151 | expectedVer: "2.4.2", 152 | expectError: false, 153 | }, 154 | { 155 | name: "Complex pattern - 7", 156 | pattern: "(?:((?:\\d+\\.)+\\d+)\\/(?:dc\\/)?)?dc(?:\\.leaflet)?\\.js\\;version:\\1", 157 | target: "/ajax/libs/dc/2.1.8/dc.leaflet.js", 158 | expectedVer: "2.1.8", 159 | expectError: false, 160 | }, 161 | { 162 | name: "Complex pattern - 8", 163 | pattern: "(?:(\\d+(?:\\.\\d+)+)\\/(?:dc\\/)?)?dc(?:\\.leaflet)?\\.js\\;version:\\1", 164 | target: "/ajax/libs/dc/2.1.8/dc.leaflet.js", 165 | expectedVer: "2.1.8", 166 | expectError: false, 167 | }, 168 | } 169 | 170 | for _, tt := range tests { 171 | t.Run(tt.name, func(t *testing.T) { 172 | p, err := ParsePattern(tt.pattern) 173 | if err != nil { 174 | t.Fatal("Failed to parse pattern:", err) 175 | } 176 | 177 | match, ver := p.Evaluate(tt.target) 178 | if !match { 179 | t.Errorf("Failed to match pattern %s with target %s", tt.pattern, tt.target) 180 | return 181 | } 182 | if (err != nil) != tt.expectError { 183 | t.Errorf("extractVersion() error = %v, expectError %v", err, tt.expectError) 184 | return 185 | } 186 | if ver != tt.expectedVer { 187 | t.Errorf("Expected version = %s, got %s", tt.expectedVer, ver) 188 | } 189 | }) 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /tech.go: -------------------------------------------------------------------------------- 1 | package wappalyzer 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "os" 8 | "strings" 9 | ) 10 | 11 | // Wappalyze is a client for working with tech detection 12 | type Wappalyze struct { 13 | original *Fingerprints 14 | fingerprints *CompiledFingerprints 15 | } 16 | 17 | // New creates a new tech detection instance 18 | func New() (*Wappalyze, error) { 19 | wappalyze := &Wappalyze{ 20 | fingerprints: &CompiledFingerprints{ 21 | Apps: make(map[string]*CompiledFingerprint), 22 | }, 23 | } 24 | 25 | err := wappalyze.loadFingerprints() 26 | if err != nil { 27 | return nil, err 28 | } 29 | return wappalyze, nil 30 | } 31 | 32 | // NewFromFile creates a new tech detection instance from a file 33 | // this allows using the latest fingerprints without recompiling the code 34 | // loadEmbedded indicates whether to load the embedded fingerprints 35 | // supersede indicates whether to overwrite the embedded fingerprints (if loaded) with the file fingerprints if the app name conflicts 36 | // supersede is only used if loadEmbedded is true 37 | func NewFromFile(filePath string, loadEmbedded, supersede bool) (*Wappalyze, error) { 38 | wappalyze := &Wappalyze{ 39 | fingerprints: &CompiledFingerprints{ 40 | Apps: make(map[string]*CompiledFingerprint), 41 | }, 42 | } 43 | 44 | err := wappalyze.loadFingerprintsFromFile(filePath, loadEmbedded, supersede) 45 | if err != nil { 46 | return nil, err 47 | } 48 | 49 | return wappalyze, nil 50 | } 51 | 52 | // GetFingerprints returns the original fingerprints 53 | func (s *Wappalyze) GetFingerprints() *Fingerprints { 54 | return s.original 55 | } 56 | 57 | // GetCompiledFingerprints returns the compiled fingerprints 58 | func (s *Wappalyze) GetCompiledFingerprints() *CompiledFingerprints { 59 | return s.fingerprints 60 | } 61 | 62 | // loadFingerprints loads the fingerprints and compiles them 63 | func (s *Wappalyze) loadFingerprints() error { 64 | var fingerprintsStruct Fingerprints 65 | err := json.Unmarshal([]byte(fingerprints), &fingerprintsStruct) 66 | if err != nil { 67 | return err 68 | } 69 | 70 | s.original = &fingerprintsStruct 71 | for i, fingerprint := range fingerprintsStruct.Apps { 72 | s.fingerprints.Apps[i] = compileFingerprint(fingerprint) 73 | } 74 | return nil 75 | } 76 | 77 | // loadFingerprints loads the fingerprints from the provided file and compiles them 78 | func (s *Wappalyze) loadFingerprintsFromFile(filePath string, loadEmbedded, supersede bool) error { 79 | 80 | f, err := os.ReadFile(filePath) 81 | if err != nil { 82 | return err 83 | } 84 | 85 | var fingerprintsStruct Fingerprints 86 | err = json.Unmarshal(f, &fingerprintsStruct) 87 | if err != nil { 88 | return err 89 | } 90 | 91 | if len(fingerprintsStruct.Apps) == 0 { 92 | return fmt.Errorf("no fingerprints found in file: %s", filePath) 93 | } 94 | 95 | if loadEmbedded { 96 | var embedded Fingerprints 97 | err := json.Unmarshal([]byte(fingerprints), &embedded) 98 | if err != nil { 99 | return err 100 | } 101 | 102 | s.original = &embedded 103 | 104 | for app, fingerprint := range fingerprintsStruct.Apps { 105 | if _, ok := s.original.Apps[app]; ok && supersede { 106 | s.original.Apps[app] = fingerprint 107 | } else { 108 | s.original.Apps[app] = fingerprint 109 | } 110 | } 111 | 112 | } else { 113 | s.original = &fingerprintsStruct 114 | } 115 | 116 | for i, fingerprint := range s.original.Apps { 117 | s.fingerprints.Apps[i] = compileFingerprint(fingerprint) 118 | } 119 | 120 | return nil 121 | } 122 | 123 | // Fingerprint identifies technologies on a target, 124 | // based on the received response headers and body. 125 | // 126 | // Body should not be mutated while this function is being called, or it may 127 | // lead to unexpected things. 128 | func (s *Wappalyze) Fingerprint(headers map[string][]string, body []byte) map[string]struct{} { 129 | uniqueFingerprints := NewUniqueFingerprints() 130 | 131 | // Lowercase everything that we have received to check 132 | normalizedBody := bytes.ToLower(body) 133 | normalizedHeaders := s.normalizeHeaders(headers) 134 | 135 | // Run header based fingerprinting if the number 136 | // of header checks if more than 0. 137 | for _, app := range s.checkHeaders(normalizedHeaders) { 138 | uniqueFingerprints.SetIfNotExists(app.application, app.version, app.confidence) 139 | } 140 | 141 | cookies := s.findSetCookie(normalizedHeaders) 142 | // Run cookie based fingerprinting if we have a set-cookie header 143 | if len(cookies) > 0 { 144 | for _, app := range s.checkCookies(cookies) { 145 | uniqueFingerprints.SetIfNotExists(app.application, app.version, app.confidence) 146 | } 147 | } 148 | 149 | // Check for stuff in the body finally 150 | bodyTech := s.checkBody(normalizedBody) 151 | for _, app := range bodyTech { 152 | uniqueFingerprints.SetIfNotExists(app.application, app.version, app.confidence) 153 | } 154 | return uniqueFingerprints.GetValues() 155 | } 156 | 157 | type UniqueFingerprints struct { 158 | values map[string]uniqueFingerprintMetadata 159 | } 160 | 161 | type uniqueFingerprintMetadata struct { 162 | confidence int 163 | version string 164 | } 165 | 166 | func NewUniqueFingerprints() UniqueFingerprints { 167 | return UniqueFingerprints{ 168 | values: make(map[string]uniqueFingerprintMetadata), 169 | } 170 | } 171 | 172 | func (u UniqueFingerprints) GetValues() map[string]struct{} { 173 | values := make(map[string]struct{}, len(u.values)) 174 | for k, v := range u.values { 175 | if v.confidence == 0 { 176 | continue 177 | } 178 | values[FormatAppVersion(k, v.version)] = struct{}{} 179 | } 180 | return values 181 | } 182 | 183 | const versionSeparator = ":" 184 | 185 | func (u UniqueFingerprints) SetIfNotExists(value, version string, confidence int) { 186 | if _, ok := u.values[value]; ok { 187 | new := u.values[value] 188 | updatedConfidence := new.confidence + confidence 189 | if updatedConfidence > 100 { 190 | updatedConfidence = 100 191 | } 192 | new.confidence = updatedConfidence 193 | if new.version == "" && version != "" { 194 | new.version = version 195 | } 196 | u.values[value] = new 197 | return 198 | } 199 | 200 | u.values[value] = uniqueFingerprintMetadata{ 201 | confidence: confidence, 202 | version: version, 203 | } 204 | } 205 | 206 | type matchPartResult struct { 207 | application string 208 | confidence int 209 | version string 210 | } 211 | 212 | // FingerprintWithTitle identifies technologies on a target, 213 | // based on the received response headers and body. 214 | // It also returns the title of the page. 215 | // 216 | // Body should not be mutated while this function is being called, or it may 217 | // lead to unexpected things. 218 | func (s *Wappalyze) FingerprintWithTitle(headers map[string][]string, body []byte) (map[string]struct{}, string) { 219 | uniqueFingerprints := NewUniqueFingerprints() 220 | 221 | // Lowercase everything that we have received to check 222 | normalizedBody := bytes.ToLower(body) 223 | normalizedHeaders := s.normalizeHeaders(headers) 224 | 225 | // Run header based fingerprinting if the number 226 | // of header checks if more than 0. 227 | for _, app := range s.checkHeaders(normalizedHeaders) { 228 | uniqueFingerprints.SetIfNotExists(app.application, app.version, app.confidence) 229 | } 230 | 231 | cookies := s.findSetCookie(normalizedHeaders) 232 | // Run cookie based fingerprinting if we have a set-cookie header 233 | if len(cookies) > 0 { 234 | for _, app := range s.checkCookies(cookies) { 235 | uniqueFingerprints.SetIfNotExists(app.application, app.version, app.confidence) 236 | } 237 | } 238 | 239 | // Check for stuff in the body finally 240 | if strings.Contains(normalizedHeaders["content-type"], "text/html") { 241 | bodyTech := s.checkBody(normalizedBody) 242 | for _, app := range bodyTech { 243 | uniqueFingerprints.SetIfNotExists(app.application, app.version, app.confidence) 244 | } 245 | title := s.getTitle(body) 246 | return uniqueFingerprints.GetValues(), title 247 | } 248 | return uniqueFingerprints.GetValues(), "" 249 | } 250 | 251 | // FingerprintWithInfo identifies technologies on a target, 252 | // based on the received response headers and body. 253 | // It also returns basic information about the technology, such as description 254 | // and website URL as well as icon. 255 | // 256 | // Body should not be mutated while this function is being called, or it may 257 | // lead to unexpected things. 258 | func (s *Wappalyze) FingerprintWithInfo(headers map[string][]string, body []byte) map[string]AppInfo { 259 | apps := s.Fingerprint(headers, body) 260 | result := make(map[string]AppInfo, len(apps)) 261 | 262 | for app := range apps { 263 | if fingerprint, ok := s.fingerprints.Apps[app]; ok { 264 | result[app] = AppInfoFromFingerprint(fingerprint) 265 | } 266 | 267 | // Handle colon separated values 268 | if strings.Contains(app, versionSeparator) { 269 | if parts := strings.Split(app, versionSeparator); len(parts) == 2 { 270 | if fingerprint, ok := s.fingerprints.Apps[parts[0]]; ok { 271 | result[app] = AppInfoFromFingerprint(fingerprint) 272 | } 273 | } 274 | } 275 | } 276 | 277 | return result 278 | } 279 | 280 | func AppInfoFromFingerprint(fingerprint *CompiledFingerprint) AppInfo { 281 | categories := make([]string, 0, len(fingerprint.cats)) 282 | for _, cat := range fingerprint.cats { 283 | if category, ok := categoriesMapping[cat]; ok { 284 | categories = append(categories, category.Name) 285 | } 286 | } 287 | return AppInfo{ 288 | Description: fingerprint.description, 289 | Website: fingerprint.website, 290 | Icon: fingerprint.icon, 291 | CPE: fingerprint.cpe, 292 | Categories: categories, 293 | } 294 | } 295 | 296 | // FingerprintWithCats identifies technologies on a target, 297 | // based on the received response headers and body. 298 | // It also returns categories information about the technology, is there's any 299 | // Body should not be mutated while this function is being called, or it may 300 | // lead to unexpected things. 301 | func (s *Wappalyze) FingerprintWithCats(headers map[string][]string, body []byte) map[string]CatsInfo { 302 | apps := s.Fingerprint(headers, body) 303 | result := make(map[string]CatsInfo, len(apps)) 304 | 305 | for app := range apps { 306 | if fingerprint, ok := s.fingerprints.Apps[app]; ok { 307 | result[app] = CatsInfo{ 308 | Cats: fingerprint.cats, 309 | } 310 | } 311 | } 312 | 313 | return result 314 | } 315 | -------------------------------------------------------------------------------- /wappalyzergo_test.go: -------------------------------------------------------------------------------- 1 | package wappalyzer 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestCookiesDetect(t *testing.T) { 10 | wappalyzer, err := New() 11 | require.Nil(t, err, "could not create wappalyzer") 12 | 13 | matches := wappalyzer.Fingerprint(map[string][]string{ 14 | "Set-Cookie": {"_uetsid=ABCDEF"}, 15 | }, []byte("")) 16 | require.Contains(t, matches, "Microsoft Advertising", "Could not get correct match") 17 | 18 | t.Run("position", func(t *testing.T) { 19 | wappalyzerClient, _ := New() 20 | 21 | fingerprints := wappalyzerClient.Fingerprint(map[string][]string{ 22 | "Set-Cookie": {"path=/; jsessionid=111; path=/, jsessionid=111;"}, 23 | }, []byte("")) 24 | fingerprints1 := wappalyzerClient.Fingerprint(map[string][]string{ 25 | "Set-Cookie": {"jsessionid=111; path=/, XSRF-TOKEN=; expires=test, path=/ laravel_session=eyJ*"}, 26 | }, []byte("")) 27 | 28 | require.Equal(t, map[string]struct{}{"Java": {}}, fingerprints, "could not get correct fingerprints") 29 | require.Equal(t, map[string]struct{}{"Java": {}, "Laravel": {}, "PHP": {}}, fingerprints1, "could not get correct fingerprints") 30 | }) 31 | } 32 | 33 | func TestHeadersDetect(t *testing.T) { 34 | wappalyzer, err := New() 35 | require.Nil(t, err, "could not create wappalyzer") 36 | 37 | matches := wappalyzer.Fingerprint(map[string][]string{ 38 | "Server": {"now"}, 39 | }, []byte("")) 40 | 41 | require.Contains(t, matches, "Vercel", "Could not get correct match") 42 | } 43 | 44 | func TestBodyDetect(t *testing.T) { 45 | wappalyzer, err := New() 46 | require.Nil(t, err, "could not create wappalyzer") 47 | 48 | t.Run("meta", func(t *testing.T) { 49 | matches := wappalyzer.Fingerprint(map[string][]string{}, []byte(` 50 | 51 | 52 | 53 | `)) 54 | require.Contains(t, matches, "Mura CMS:1", "Could not get correct match") 55 | }) 56 | 57 | t.Run("html-implied", func(t *testing.T) { 58 | matches := wappalyzer.Fingerprint(map[string][]string{}, []byte(` 59 | 60 | 61 | 62 | 63 | `)) 64 | require.Contains(t, matches, "AngularJS", "Could not get correct implied match") 65 | require.Contains(t, matches, "PHP", "Could not get correct implied match") 66 | require.Contains(t, matches, "Proximis Unified Commerce", "Could not get correct match") 67 | }) 68 | } 69 | 70 | func TestUniqueFingerprints(t *testing.T) { 71 | fingerprints := NewUniqueFingerprints() 72 | fingerprints.SetIfNotExists("test", "", 100) 73 | require.Equal(t, map[string]struct{}{"test": {}}, fingerprints.GetValues(), "could not get correct values") 74 | 75 | t.Run("linear", func(t *testing.T) { 76 | fingerprints.SetIfNotExists("new", "2.3.5", 100) 77 | require.Equal(t, map[string]struct{}{"test": {}, "new:2.3.5": {}}, fingerprints.GetValues(), "could not get correct values") 78 | 79 | fingerprints.SetIfNotExists("new", "", 100) 80 | require.Equal(t, map[string]struct{}{"test": {}, "new:2.3.5": {}}, fingerprints.GetValues(), "could not get correct values") 81 | }) 82 | 83 | t.Run("opposite", func(t *testing.T) { 84 | fingerprints.SetIfNotExists("another", "", 100) 85 | require.Equal(t, map[string]struct{}{"test": {}, "new:2.3.5": {}, "another": {}}, fingerprints.GetValues(), "could not get correct values") 86 | 87 | fingerprints.SetIfNotExists("another", "2.3.5", 100) 88 | require.Equal(t, map[string]struct{}{"test": {}, "new:2.3.5": {}, "another:2.3.5": {}}, fingerprints.GetValues(), "could not get correct values") 89 | }) 90 | 91 | t.Run("confidence", func(t *testing.T) { 92 | f := NewUniqueFingerprints() 93 | f.SetIfNotExists("test", "", 0) 94 | require.Equal(t, map[string]struct{}{}, f.GetValues(), "could not get correct values") 95 | 96 | f.SetIfNotExists("test", "2.36.4", 100) 97 | require.Equal(t, map[string]struct{}{"test:2.36.4": {}}, f.GetValues(), "could not get correct values") 98 | }) 99 | } 100 | 101 | func Test_FingerprintWithInfo(t *testing.T) { 102 | wappalyzer, err := New() 103 | require.Nil(t, err, "could not create wappalyzer") 104 | 105 | name := "Liferay:7.3.5" 106 | matches := wappalyzer.FingerprintWithInfo(map[string][]string{ 107 | "liferay-portal": {"testserver 7.3.5"}, 108 | }, []byte("")) 109 | require.Contains(t, matches, name, "Could not get correct match") 110 | 111 | value := matches[name] 112 | require.Equal(t, "cpe:2.3:a:liferay:liferay_portal:*:*:*:*:*:*:*:*", value.CPE, "could not get correct name") 113 | require.Equal(t, "https://www.liferay.com/", value.Website, "could not get correct website") 114 | require.Equal(t, "Liferay is an open-source company that provides free documentation and paid professional service to users of its software.", value.Description, "could not get correct description") 115 | require.Equal(t, "Liferay.svg", value.Icon, "could not get correct icon") 116 | require.ElementsMatch(t, []string{"CMS"}, value.Categories, "could not get correct categories") 117 | } 118 | --------------------------------------------------------------------------------