``` ├── .github/ ├── ISSUE_TEMPLATE/ ├── config.yml ├── feature_request.md ├── issue-report.md ├── dependabot.yml ├── release.yml ├── workflows/ ├── build-test.yml ├── codeql-analysis.yml ├── compat-checks.yaml ├── dep-auto-merge.yml ├── dockerhub-push.yml ├── functional-test.yml ├── release-binary.yml ├── release-test.yml ├── security-crawl-maze-score.yaml ├── .gitignore ├── .goreleaser/ ├── linux.yml ├── mac.yml ├── windows.yml ├── Dockerfile ├── LICENSE.md ├── Makefile ├── README.md ├── SECURITY.md ├── cmd/ ├── functional-test/ ├── main.go ├── run.sh ├── integration-test/ ├── filters.go ├── integration-test.go ├── library.go ├── katana/ ├── main.go ├── tools/ ├── crawl-maze-score/ ├── main.go ├── go.mod ├── go.sum ├── integration_tests/ ├── run.sh ├── internal/ ├── runner/ ├── banner.go ├── executer.go ├── healthcheck.go ├── options.go ├── runner.go ├── testutils/ ├── helper.go ├── integration.go ├── testutils.go ├── pkg/ ├── engine/ ├── common/ ├── base.go ├── error.go ├── http.go ├── engine.go ├── hybrid/ ├── crawl.go ├── doc.go ├── hijack.go ├── hybrid.go ├── parser/ ├── files/ ├── request.go ├── robotstxt.go ├── robotstxt_test.go ├── sitemapxml.go ├── sitemapxml_test.go ├── parser.go ├── parser_generic.go ``` ## /.github/ISSUE_TEMPLATE/config.yml ```yml path="/.github/ISSUE_TEMPLATE/config.yml" blank_issues_enabled: false contact_links: - name: Ask an question / advise on using katana url: https://github.com/projectdiscovery/katana/discussions/categories/q-a about: Ask a question or request support for using katana - name: Share idea / feature to discuss for katana url: https://github.com/projectdiscovery/katana/discussions/categories/ideas about: Share idea / feature to discuss for katana - name: Connect with PD Team (Discord) url: https://discord.gg/projectdiscovery about: Connect with PD Team for direct communication ``` ## /.github/ISSUE_TEMPLATE/feature_request.md --- name: Feature request about: Request feature to implement in this project labels: 'Type: Enhancement' --- ### Please describe your feature request: ### Describe the use case of this feature: ## /.github/ISSUE_TEMPLATE/issue-report.md --- name: Issue report about: Create a report to help us to improve the project labels: 'Type: Bug' --- ### katana version: ### Current Behavior: ### Expected Behavior: ### Steps To Reproduce: ### Anything else: ## /.github/dependabot.yml ```yml path="/.github/dependabot.yml" # To get started with Dependabot version updates, you'll need to specify which # package ecosystems to update and where the package manifests are located. # Please see the documentation for all configuration options: # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates version: 2 updates: # Maintain dependencies for go modules - package-ecosystem: "gomod" directory: "/" schedule: interval: "weekly" target-branch: "dev" commit-message: prefix: "chore" include: "scope" labels: - "Type: Maintenance" allow: - dependency-name: "github.com/projectdiscovery/*" # Maintain dependencies for GitHub Actions - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" target-branch: "dev" commit-message: prefix: "chore" include: "scope" labels: - "Type: Maintenance" # Maintain dependencies for docker - package-ecosystem: "docker" directory: "/" schedule: interval: "weekly" target-branch: "dev" commit-message: prefix: "chore" include: "scope" labels: - "Type: Maintenance" ``` ## /.github/release.yml ```yml path="/.github/release.yml" changelog: exclude: authors: - app/dependabot - dependabot categories: - title: 🎉 New Features labels: - "Type: Enhancement" - title: 🐞 Bug Fixes labels: - "Type: Bug" - title: 🔨 Maintenance labels: - "Type: Maintenance" - title: Other Changes labels: - "*" ``` ## /.github/workflows/build-test.yml ```yml path="/.github/workflows/build-test.yml" name: 🔨 Build Test on: workflow_dispatch: pull_request: branches: - dev paths: - '**.go' - '**.mod' jobs: lint: name: "Lint" if: "${{ !endsWith(github.actor, '[bot]') }}" runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: projectdiscovery/actions/setup/go@v1 - uses: projectdiscovery/actions/golangci-lint@v1 build: name: Test Builds needs: [lint] runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest, windows-latest, macOS-latest] go-version: [1.21.x] steps: - name: Set up Go uses: actions/setup-go@v4 with: go-version: ${{ matrix.go-version }} - name: Check out code uses: actions/checkout@v4 - name: Test run: go test ./... working-directory: . - name: Build run: go build . working-directory: cmd/katana/ - name: Integration Tests env: GH_ACTION: true run: bash run.sh working-directory: integration_tests/ - name: Install run: go install working-directory: cmd/katana/ - name: Race Condition Tests run: go build -race . working-directory: cmd/katana/ ``` ## /.github/workflows/codeql-analysis.yml ```yml path="/.github/workflows/codeql-analysis.yml" name: 🚨 CodeQL Analysis on: workflow_dispatch: pull_request: branches: - dev paths: - '**.go' - '**.mod' jobs: analyze: name: Analyze runs-on: ubuntu-latest permissions: actions: read contents: read security-events: write strategy: fail-fast: false matrix: language: [ 'go' ] # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] steps: - name: Checkout repository uses: actions/checkout@v4 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v2 with: languages: ${{ matrix.language }} - name: Autobuild uses: github/codeql-action/autobuild@v2 - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v2 ``` ## /.github/workflows/compat-checks.yaml ```yaml path="/.github/workflows/compat-checks.yaml" name: ♾️ Compatibility Checks on: pull_request: types: [opened, synchronize] branches: - dev jobs: check: if: github.actor == 'dependabot[bot]' runs-on: ubuntu-latest permissions: contents: write steps: - uses: actions/checkout@v4 - uses: projectdiscovery/actions/setup/go/compat-checks@v1 with: release-test: true ``` ## /.github/workflows/dep-auto-merge.yml ```yml path="/.github/workflows/dep-auto-merge.yml" name: 🤖 Auto Merge on: pull_request_review: types: [submitted] workflow_run: workflows: ["♾️ Compatibility Check"] types: - completed permissions: pull-requests: write issues: write repository-projects: write jobs: auto-merge: runs-on: ubuntu-latest if: github.actor == 'dependabot[bot]' steps: - uses: actions/checkout@v4 with: token: ${{ secrets.DEPENDABOT_PAT }} - uses: ahmadnassri/action-dependabot-auto-merge@v2 with: github-token: ${{ secrets.DEPENDABOT_PAT }} target: all ``` ## /.github/workflows/dockerhub-push.yml ```yml path="/.github/workflows/dockerhub-push.yml" name: 🌥 Docker Push on: workflow_run: workflows: ["🎉 Release Binary"] types: - completed workflow_dispatch: jobs: docker: runs-on: ubuntu-latest-16-cores steps: - name: Checkout uses: actions/checkout@v4 - name: Get Github tag id: meta run: | curl --silent "https://api.github.com/repos/projectdiscovery/katana/releases/latest" | jq -r .tag_name | xargs -I {} echo TAG={} >> $GITHUB_OUTPUT - name: Set up QEMU uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Login to DockerHub uses: docker/login-action@v3 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_TOKEN }} - name: Build and push uses: docker/build-push-action@v6 with: context: . platforms: linux/amd64,linux/arm64 push: true tags: projectdiscovery/katana:latest,projectdiscovery/katana:${{ steps.meta.outputs.TAG }} ``` ## /.github/workflows/functional-test.yml ```yml path="/.github/workflows/functional-test.yml" name: 🧪 Functional Test on: pull_request: paths: - '**.go' - '**.mod' workflow_dispatch: jobs: functional: name: Functional Test runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest, windows-latest, macOS-latest] steps: - name: Set up Go uses: actions/setup-go@v4 with: go-version: 1.21.x - name: Check out code uses: actions/checkout@v4 - name: Functional Tests run: | chmod +x run.sh bash run.sh ${{ matrix.os }} working-directory: cmd/functional-test ``` ## /.github/workflows/release-binary.yml ```yml path="/.github/workflows/release-binary.yml" name: 🎉 Release Binary on: push: tags: - v* workflow_dispatch: jobs: build-mac: runs-on: macos-latest steps: - name: "Check out code" uses: actions/checkout@v4 with: fetch-depth: 0 - name: "Set up Go" uses: actions/setup-go@v4 with: go-version: 1.21.x cache: true - name: "Create release on GitHub" uses: goreleaser/goreleaser-action@v4 with: args: "release -f .goreleaser/mac.yml --clean" version: latest workdir: . env: GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" build-windows: runs-on: windows-latest-8-cores steps: - name: "Check out code" uses: actions/checkout@v4 with: fetch-depth: 0 - name: "Set up Go" uses: actions/setup-go@v4 with: go-version: 1.21.x cache: true - name: "Create release on GitHub" uses: goreleaser/goreleaser-action@v4 with: args: "release -f .goreleaser/windows.yml --clean" version: latest workdir: . env: GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" build-linux: runs-on: ubuntu-latest-16-cores steps: - name: "Check out code" uses: actions/checkout@v4 with: fetch-depth: 0 - name: "Set up Go" uses: actions/setup-go@v4 with: go-version: 1.21.x cache: true # todo: musl compatible? - name: Install Dependences run: sudo apt install gcc-aarch64-linux-gnu - name: "Create release on GitHub" uses: goreleaser/goreleaser-action@v4 with: args: "release -f .goreleaser/linux.yml --clean" version: latest workdir: . env: GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" SLACK_WEBHOOK: "${{ secrets.RELEASE_SLACK_WEBHOOK }}" DISCORD_WEBHOOK_ID: "${{ secrets.DISCORD_WEBHOOK_ID }}" DISCORD_WEBHOOK_TOKEN: "${{ secrets.DISCORD_WEBHOOK_TOKEN }}" ``` ## /.github/workflows/release-test.yml ```yml path="/.github/workflows/release-test.yml" name: 🔨 Release Test on: pull_request: paths: - '**.yml' - '**.go' - '**.mod' workflow_dispatch: jobs: release-test-mac: runs-on: macos-latest steps: - name: "Check out code" uses: actions/checkout@v4 with: fetch-depth: 0 - name: Set up Go uses: actions/setup-go@v4 with: go-version: 1.21.x - name: release test uses: goreleaser/goreleaser-action@v4 with: args: "release -f .goreleaser/mac.yml --clean --snapshot" version: latest workdir: . env: GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" release-test-linux: runs-on: ubuntu-latest-16-cores steps: - name: "Check out code" uses: actions/checkout@v4 with: fetch-depth: 0 - name: Set up Go uses: actions/setup-go@v4 with: go-version: 1.21.x # todo: musl compatible? - name: Install Dependences run: sudo apt update && sudo apt install gcc-aarch64-linux-gnu - name: release test uses: goreleaser/goreleaser-action@v4 with: args: "release -f .goreleaser/linux.yml --clean --snapshot" version: latest workdir: . env: GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" release-test-windows: runs-on: windows-latest-8-cores steps: - name: "Check out code" uses: actions/checkout@v4 with: fetch-depth: 0 - name: Set up Go uses: actions/setup-go@v4 with: go-version: 1.21.x - name: release test uses: goreleaser/goreleaser-action@v4 with: args: "release -f .goreleaser/windows.yml --clean --snapshot" version: latest workdir: . env: GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" ``` ## /.github/workflows/security-crawl-maze-score.yaml ```yaml path="/.github/workflows/security-crawl-maze-score.yaml" name: 🙏🏻 Security Crawl Maze Score on: workflow_dispatch: jobs: build: name: Run Scoring runs-on: ubuntu-latest-16-cores steps: - name: Set up Go uses: actions/setup-go@v4 with: go-version: 1.21.x - name: Check out code uses: actions/checkout@v4 - name: Build run: go build . working-directory: cmd/katana/ - name: Run Katana Standard run: ./katana -u https://security-crawl-maze.app/ -kf all -jc -jsluice -d 10 -o output_standard.txt -cos node_modules working-directory: cmd/katana - name: Run Katana Headless run: ./katana -u https://security-crawl-maze.app/ -kf all -jc -jsluice -d 10 -headless -o output_headless.txt -cos node_modules working-directory: cmd/katana - name: Run Score run: go build .; ./crawl-maze-score ../../katana/output_standard.txt ../../katana/output_headless.txt working-directory: cmd/tools/crawl-maze-score ``` ## /.gitignore ```gitignore path="/.gitignore" security-crawl-maze/ cmd/katana/katana katana *.exe katana_*/ katana_*/ dist/ .vscode .devcontainer ``` ## /.goreleaser/linux.yml ```yml path="/.goreleaser/linux.yml" env: - GO111MODULE=on before: hooks: - go mod tidy project_name: katana builds: - id: katana-linux-amd64-generic ldflags: - -s -w binary: katana env: - CGO_ENABLED=1 main: ./cmd/katana/main.go goos: - linux goarch: - amd64 - id: katana-linux-i386-generic ldflags: - -s -w binary: katana main: ./cmd/katana/main.go goos: - linux goarch: - 386 - id: katana-linux-arm ldflags: - -s -w binary: katana env: - CGO_ENABLED=1 - CC=aarch64-linux-gnu-gcc main: ./cmd/katana/main.go goos: - linux goarch: - arm64 archives: - format: zip checksum: name_template: "{{ .ProjectName }}-linux-checksums.txt" announce: slack: enabled: true channel: '#release' username: GoReleaser message_template: 'New Release: {{ .ProjectName }} {{.Tag}} is published! Check it out at {{ .ReleaseURL }}' discord: enabled: true message_template: '**New Release: {{ .ProjectName }} {{.Tag}}** is published! Check it out at {{ .ReleaseURL }}' ``` ## /.goreleaser/mac.yml ```yml path="/.goreleaser/mac.yml" env: - GO111MODULE=on before: hooks: - go mod tidy project_name: katana builds: - id: katana-darwin ldflags: - -s -w binary: katana env: - CGO_ENABLED=1 main: ./cmd/katana/main.go goos: - darwin goarch: - amd64 - arm64 - 386 - arm archives: - format: zip name_template: '{{ .ProjectName }}_{{ .Version }}_{{ if eq .Os "darwin" }}macOS{{ else }}{{ .Os }}{{ end }}_{{ .Arch }}' checksum: name_template: "{{ .ProjectName }}-mac-checksums.txt" ``` ## /.goreleaser/windows.yml ```yml path="/.goreleaser/windows.yml" env: - GO111MODULE=on before: hooks: - go mod tidy project_name: katana builds: - id: katana-windows ldflags: - -s -w binary: katana env: - CGO_ENABLED=1 main: ./cmd/katana/main.go goos: - windows goarch: - 386 - arm64 - amd64 archives: - format: zip checksum: name_template: "{{ .ProjectName }}-windows-checksums.txt" ``` ## /Dockerfile ``` path="/Dockerfile" FROM golang:1.23-alpine AS build-env RUN apk add --no-cache git gcc musl-dev WORKDIR /app COPY . /app RUN go mod download RUN go build ./cmd/katana FROM alpine:3.21.3 RUN apk add --no-cache bind-tools ca-certificates chromium COPY --from=build-env /app/katana /usr/local/bin/ ENTRYPOINT ["katana"] ``` ## /LICENSE.md MIT License Copyright (c) 2022 ProjectDiscovery Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ## /Makefile ``` path="/Makefile" # Go parameters GOCMD=go GOBUILD=$(GOCMD) build GOMOD=$(GOCMD) mod GOTEST=$(GOCMD) test GOFLAGS := -v # This should be disabled if the binary uses pprof LDFLAGS := -s -w ifneq ($(shell go env GOOS),darwin) LDFLAGS := -extldflags "-static" endif all: build build: $(GOBUILD) $(GOFLAGS) -ldflags '$(LDFLAGS)' -o "katana" cmd/katana/main.go test: $(GOTEST) $(GOFLAGS) ./... integration: cd integration_tests; bash run.sh cd .. tidy: $(GOMOD) tidy ``` ## /README.md

katana

A next-generation crawling and spidering framework

FeaturesInstallationUsageScopeConfigFiltersJoin Discord

# Features ![image](https://user-images.githubusercontent.com/8293321/199371558-daba03b6-bf9c-4883-8506-76497c6c3a44.png) - Fast And fully configurable web crawling - **Standard** and **Headless** mode - **JavaScript** parsing / crawling - Customizable **automatic form filling** - **Scope control** - Preconfigured field / Regex - **Customizable output** - Preconfigured fields - INPUT - **STDIN**, **URL** and **LIST** - OUTPUT - **STDOUT**, **FILE** and **JSON** ## Installation katana requires Go 1.21+ to install successfully. If you encounter any installation issues, we recommend trying with the latest available version of Go, as the minimum required version may have changed. Run the command below or download a pre-compiled binary from the [release page](https://github.com/projectdiscovery/katana/releases). ```console CGO_ENABLED=1 go install github.com/projectdiscovery/katana/cmd/katana@latest ``` **More options to install / run katana-**
Docker > To install / update docker to latest tag - ```sh docker pull projectdiscovery/katana:latest ``` > To run katana in standard mode using docker - ```sh docker run projectdiscovery/katana:latest -u https://tesla.com ``` > To run katana in headless mode using docker - ```sh docker run projectdiscovery/katana:latest -u https://tesla.com -system-chrome -headless ```
Ubuntu > It's recommended to install the following prerequisites - ```sh sudo apt update sudo snap refresh sudo apt install zip curl wget git sudo snap install golang --classic wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | sudo apt-key add - sudo sh -c 'echo "deb http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' sudo apt update sudo apt install google-chrome-stable ``` > install katana - ```sh go install github.com/projectdiscovery/katana/cmd/katana@latest ```
## Usage ```console katana -h ``` This will display help for the tool. Here are all the switches it supports. ```console Katana is a fast crawler focused on execution in automation pipelines offering both headless and non-headless crawling. Usage: ./katana [flags] Flags: INPUT: -u, -list string[] target url / list to crawl -resume string resume scan using resume.cfg -e, -exclude string[] exclude host matching specified filter ('cdn', 'private-ips', cidr, ip, regex) CONFIGURATION: -r, -resolvers string[] list of custom resolver (file or comma separated) -d, -depth int maximum depth to crawl (default 3) -jc, -js-crawl enable endpoint parsing / crawling in javascript file -jsl, -jsluice enable jsluice parsing in javascript file (memory intensive) -ct, -crawl-duration value maximum duration to crawl the target for (s, m, h, d) (default s) -kf, -known-files string enable crawling of known files (all,robotstxt,sitemapxml), a minimum depth of 3 is required to ensure all known files are properly crawled. -mrs, -max-response-size int maximum response size to read (default 9223372036854775807) -timeout int time to wait for request in seconds (default 10) -aff, -automatic-form-fill enable automatic form filling (experimental) -fx, -form-extraction extract form, input, textarea & select elements in jsonl output -retry int number of times to retry the request (default 1) -proxy string http/socks5 proxy to use -H, -headers string[] custom header/cookie to include in all http request in header:value format (file) -config string path to the katana configuration file -fc, -form-config string path to custom form configuration file -flc, -field-config string path to custom field configuration file -s, -strategy string Visit strategy (depth-first, breadth-first) (default "depth-first") -iqp, -ignore-query-params Ignore crawling same path with different query-param values -tlsi, -tls-impersonate enable experimental client hello (ja3) tls randomization -dr, -disable-redirects disable following redirects (default false) DEBUG: -health-check, -hc run diagnostic check up -elog, -error-log string file to write sent requests error log HEADLESS: -hl, -headless enable headless hybrid crawling (experimental) -sc, -system-chrome use local installed chrome browser instead of katana installed -sb, -show-browser show the browser on the screen with headless mode -ho, -headless-options string[] start headless chrome with additional options -nos, -no-sandbox start headless chrome in --no-sandbox mode -cdd, -chrome-data-dir string path to store chrome browser data -scp, -system-chrome-path string use specified chrome browser for headless crawling -noi, -no-incognito start headless chrome without incognito mode -cwu, -chrome-ws-url string use chrome browser instance launched elsewhere with the debugger listening at this URL -xhr, -xhr-extraction extract xhr request url,method in jsonl output SCOPE: -cs, -crawl-scope string[] in scope url regex to be followed by crawler -cos, -crawl-out-scope string[] out of scope url regex to be excluded by crawler -fs, -field-scope string pre-defined scope field (dn,rdn,fqdn) or custom regex (e.g., '(company-staging.io|company.com)') (default "rdn") -ns, -no-scope disables host based default scope -do, -display-out-scope display external endpoint from scoped crawling FILTER: -mr, -match-regex string[] regex or list of regex to match on output url (cli, file) -fr, -filter-regex string[] regex or list of regex to filter on output url (cli, file) -f, -field string field to display in output (url,path,fqdn,rdn,rurl,qurl,qpath,file,ufile,key,value,kv,dir,udir) -sf, -store-field string field to store in per-host output (url,path,fqdn,rdn,rurl,qurl,qpath,file,ufile,key,value,kv,dir,udir) -em, -extension-match string[] match output for given extension (eg, -em php,html,js) -ef, -extension-filter string[] filter output for given extension (eg, -ef png,css) -mdc, -match-condition string match response with dsl based condition -fdc, -filter-condition string filter response with dsl based condition RATE-LIMIT: -c, -concurrency int number of concurrent fetchers to use (default 10) -p, -parallelism int number of concurrent inputs to process (default 10) -rd, -delay int request delay between each request in seconds -rl, -rate-limit int maximum requests to send per second (default 150) -rlm, -rate-limit-minute int maximum number of requests to send per minute UPDATE: -up, -update update katana to latest version -duc, -disable-update-check disable automatic katana update check OUTPUT: -o, -output string file to write output to -sr, -store-response store http requests/responses -srd, -store-response-dir string store http requests/responses to custom directory -sfd, -store-field-dir string store per-host field to custom directory -or, -omit-raw omit raw requests/responses from jsonl output -ob, -omit-body omit response body from jsonl output -j, -jsonl write output in jsonl format -nc, -no-color disable output content coloring (ANSI escape codes) -silent display output only -v, -verbose display verbose output -debug display debug output -version display project version ``` ## Running Katana ### Input for katana **katana** requires **url** or **endpoint** to crawl and accepts single or multiple inputs. Input URL can be provided using `-u` option, and multiple values can be provided using comma-separated input, similarly **file** input is supported using `-list` option and additionally piped input (stdin) is also supported. #### URL Input ```sh katana -u https://tesla.com ``` #### Multiple URL Input (comma-separated) ```sh katana -u https://tesla.com,https://google.com ``` #### List Input ```bash $ cat url_list.txt https://tesla.com https://google.com ``` ``` katana -list url_list.txt ``` #### STDIN (piped) Input ```sh echo https://tesla.com | katana ``` ```sh cat domains | httpx | katana ``` Example running katana - ```console katana -u https://youtube.com __ __ / /_____ _/ /____ ____ ___ _ / '_/ _ / __/ _ / _ \/ _ / /_/\_\\_,_/\__/\_,_/_//_/\_,_/ v0.0.1 projectdiscovery.io [WRN] Use with caution. You are responsible for your actions. [WRN] Developers assume no liability and are not responsible for any misuse or damage. https://www.youtube.com/ https://www.youtube.com/about/ https://www.youtube.com/about/press/ https://www.youtube.com/about/copyright/ https://www.youtube.com/t/contact_us/ https://www.youtube.com/creators/ https://www.youtube.com/ads/ https://www.youtube.com/t/terms https://www.youtube.com/t/privacy https://www.youtube.com/about/policies/ https://www.youtube.com/howyoutubeworks?utm_campaign=ytgen&utm_source=ythp&utm_medium=LeftNav&utm_content=txt&u=https%3A%2F%2Fwww.youtube.com%2Fhowyoutubeworks%3Futm_source%3Dythp%26utm_medium%3DLeftNav%26utm_campaign%3Dytgen https://www.youtube.com/new https://m.youtube.com/ https://www.youtube.com/s/desktop/4965577f/jsbin/desktop_polymer.vflset/desktop_polymer.js https://www.youtube.com/s/desktop/4965577f/cssbin/www-main-desktop-home-page-skeleton.css https://www.youtube.com/s/desktop/4965577f/cssbin/www-onepick.css https://www.youtube.com/s/_/ytmainappweb/_/ss/k=ytmainappweb.kevlar_base.0Zo5FUcPkCg.L.B1.O/am=gAE/d=0/rs=AGKMywG5nh5Qp-BGPbOaI1evhF5BVGRZGA https://www.youtube.com/opensearch?locale=en_GB https://www.youtube.com/manifest.webmanifest https://www.youtube.com/s/desktop/4965577f/cssbin/www-main-desktop-watch-page-skeleton.css https://www.youtube.com/s/desktop/4965577f/jsbin/web-animations-next-lite.min.vflset/web-animations-next-lite.min.js https://www.youtube.com/s/desktop/4965577f/jsbin/custom-elements-es5-adapter.vflset/custom-elements-es5-adapter.js https://www.youtube.com/s/desktop/4965577f/jsbin/webcomponents-sd.vflset/webcomponents-sd.js https://www.youtube.com/s/desktop/4965577f/jsbin/intersection-observer.min.vflset/intersection-observer.min.js https://www.youtube.com/s/desktop/4965577f/jsbin/scheduler.vflset/scheduler.js https://www.youtube.com/s/desktop/4965577f/jsbin/www-i18n-constants-en_GB.vflset/www-i18n-constants.js https://www.youtube.com/s/desktop/4965577f/jsbin/www-tampering.vflset/www-tampering.js https://www.youtube.com/s/desktop/4965577f/jsbin/spf.vflset/spf.js https://www.youtube.com/s/desktop/4965577f/jsbin/network.vflset/network.js https://www.youtube.com/howyoutubeworks/ https://www.youtube.com/trends/ https://www.youtube.com/jobs/ https://www.youtube.com/kids/ ``` ## Crawling Mode ### Standard Mode Standard crawling modality uses the standard go http library under the hood to handle HTTP requests/responses. This modality is much faster as it doesn't have the browser overhead. Still, it analyzes HTTP responses body as is, without any javascript or DOM rendering, potentially missing post-dom-rendered endpoints or asynchronous endpoint calls that might happen in complex web applications depending, for example, on browser-specific events. ### Headless Mode Headless mode hooks internal headless calls to handle HTTP requests/responses directly within the browser context. This offers two advantages: - The HTTP fingerprint (TLS and user agent) fully identify the client as a legitimate browser - Better coverage since the endpoints are discovered analyzing the standard raw response, as in the previous modality, and also the browser-rendered one with javascript enabled. Headless crawling is optional and can be enabled using `-headless` option. Here are other headless CLI options - ```console katana -h headless Flags: HEADLESS: -hl, -headless enable headless hybrid crawling (experimental) -sc, -system-chrome use local installed chrome browser instead of katana installed -sb, -show-browser show the browser on the screen with headless mode -ho, -headless-options string[] start headless chrome with additional options -nos, -no-sandbox start headless chrome in --no-sandbox mode -cdd, -chrome-data-dir string path to store chrome browser data -scp, -system-chrome-path string use specified chrome browser for headless crawling -noi, -no-incognito start headless chrome without incognito mode -cwu, -chrome-ws-url string use chrome browser instance launched elsewhere with the debugger listening at this URL -xhr, -xhr-extraction extract xhr requests ``` *`-no-sandbox`* ---- Runs headless chrome browser with **no-sandbox** option, useful when running as root user. ```console katana -u https://tesla.com -headless -no-sandbox ``` *`-no-incognito`* ---- Runs headless chrome browser without incognito mode, useful when using the local browser. ```console katana -u https://tesla.com -headless -no-incognito ``` *`-headless-options`* ---- When crawling in headless mode, additional chrome options can be specified using `-headless-options`, for example - ```console katana -u https://tesla.com -headless -system-chrome -headless-options --disable-gpu,proxy-server=http://127.0.0.1:8080 ``` ## Scope Control Crawling can be endless if not scoped, as such katana comes with multiple support to define the crawl scope. *`-field-scope`* ---- Most handy option to define scope with predefined field name, `rdn` being default option for field scope. - `rdn` - crawling scoped to root domain name and all subdomains (e.g. `*example.com`) (default) - `fqdn` - crawling scoped to given sub(domain) (e.g. `www.example.com` or `api.example.com`) - `dn` - crawling scoped to domain name keyword (e.g. `example`) ```console katana -u https://tesla.com -fs dn ``` *`-crawl-scope`* ------ For advanced scope control, `-cs` option can be used that comes with **regex** support. ```console katana -u https://tesla.com -cs login ``` For multiple in scope rules, file input with multiline string / regex can be passed. ```bash $ cat in_scope.txt login/ admin/ app/ wordpress/ ``` ```console katana -u https://tesla.com -cs in_scope.txt ``` *`-crawl-out-scope`* ----- For defining what not to crawl, `-cos` option can be used and also support **regex** input. ```console katana -u https://tesla.com -cos logout ``` For multiple out of scope rules, file input with multiline string / regex can be passed. ```bash $ cat out_of_scope.txt /logout /log_out ``` ```console katana -u https://tesla.com -cos out_of_scope.txt ``` *`-no-scope`* ---- Katana is default to scope `*.domain`, to disable this `-ns` option can be used and also to crawl the internet. ```console katana -u https://tesla.com -ns ``` *`-display-out-scope`* ---- As default, when scope option is used, it also applies for the links to display as output, as such **external URLs are default to exclude** and to overwrite this behavior, `-do` option can be used to display all the external URLs that exist in targets scoped URL / Endpoint. ``` katana -u https://tesla.com -do ``` Here is all the CLI options for the scope control - ```console katana -h scope Flags: SCOPE: -cs, -crawl-scope string[] in scope url regex to be followed by crawler -cos, -crawl-out-scope string[] out of scope url regex to be excluded by crawler -fs, -field-scope string pre-defined scope field (dn,rdn,fqdn) (default "rdn") -ns, -no-scope disables host based default scope -do, -display-out-scope display external endpoint from scoped crawling ``` ## Crawler Configuration Katana comes with multiple options to configure and control the crawl as the way we want. *`-depth`* ---- Option to define the `depth` to follow the urls for crawling, the more depth the more number of endpoint being crawled + time for crawl. ``` katana -u https://tesla.com -d 5 ``` *`-js-crawl`* ---- Option to enable JavaScript file parsing + crawling the endpoints discovered in JavaScript files, disabled as default. ``` katana -u https://tesla.com -jc ``` *`-crawl-duration`* ---- Option to predefined crawl duration, disabled as default. ``` katana -u https://tesla.com -ct 2 ``` *`-known-files`* ---- Option to enable crawling `robots.txt` and `sitemap.xml` file, disabled as default. ``` katana -u https://tesla.com -kf robotstxt,sitemapxml ``` *`-automatic-form-fill`* ---- Option to enable automatic form filling for known / unknown fields, known field values can be customized as needed by updating form config file at `$HOME/.config/katana/form-config.yaml`. Automatic form filling is experimental feature. ``` katana -u https://tesla.com -aff ``` ## Authenticated Crawling Authenticated crawling involves including custom headers or cookies in HTTP requests to access protected resources. These headers provide authentication or authorization information, allowing you to crawl authenticated content / endpoint. You can specify headers directly in the command line or provide them as a file with katana to perform authenticated crawling. > **Note**: User needs to be manually perform the authentication and export the session cookie / header to file to use with katana. *`-headers`* ---- Option to add a custom header or cookie to the request. > Syntax of [headers](https://datatracker.ietf.org/doc/html/rfc7230#section-3.2) in the HTTP specification Here is an example of adding a cookie to the request: ``` katana -u https://tesla.com -H 'Cookie: usrsess=AmljNrESo' ``` It is also possible to supply headers or cookies as a file. For example: ``` $ cat cookie.txt Cookie: PHPSESSIONID=XXXXXXXXX X-API-KEY: XXXXX TOKEN=XX ``` ``` katana -u https://tesla.com -H cookie.txt ``` There are more options to configure when needed, here is all the config related CLI options - ```console katana -h config Flags: CONFIGURATION: -r, -resolvers string[] list of custom resolver (file or comma separated) -d, -depth int maximum depth to crawl (default 3) -jc, -js-crawl enable endpoint parsing / crawling in javascript file -ct, -crawl-duration int maximum duration to crawl the target for -kf, -known-files string enable crawling of known files (all,robotstxt,sitemapxml) -mrs, -max-response-size int maximum response size to read (default 9223372036854775807) -timeout int time to wait for request in seconds (default 10) -aff, -automatic-form-fill enable automatic form filling (experimental) -fx, -form-extraction enable extraction of form, input, textarea & select elements -retry int number of times to retry the request (default 1) -proxy string http/socks5 proxy to use -H, -headers string[] custom header/cookie to include in request -config string path to the katana configuration file -fc, -form-config string path to custom form configuration file -flc, -field-config string path to custom field configuration file -s, -strategy string Visit strategy (depth-first, breadth-first) (default "depth-first") ``` ### Connecting to Active Browser Session Katana can also connect to active browser session where user is already logged in and authenticated. and use it for crawling. The only requirement for this is to start browser with remote debugging enabled. Here is an example of starting chrome browser with remote debugging enabled and using it with katana - **step 1) First Locate path of chrome executable** | Operating System | Chromium Executable Location | Google Chrome Executable Location | |------------------|------------------------------|-----------------------------------| | Windows (64-bit) | `C:\Program Files (x86)\Google\Chromium\Application\chrome.exe` | `C:\Program Files (x86)\Google\Chrome\Application\chrome.exe` | | Windows (32-bit) | `C:\Program Files\Google\Chromium\Application\chrome.exe` | `C:\Program Files\Google\Chrome\Application\chrome.exe` | | macOS | `/Applications/Chromium.app/Contents/MacOS/Chromium` | `/Applications/Google Chrome.app/Contents/MacOS/Google Chrome` | | Linux | `/usr/bin/chromium` | `/usr/bin/google-chrome` | **step 2) Start chrome with remote debugging enabled and it will return websocker url. For example, on MacOS, you can start chrome with remote debugging enabled using following command** - ```console $ /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --remote-debugging-port=9222 DevTools listening on ws://127.0.0.1:9222/devtools/browser/c5316c9c-19d6-42dc-847a-41d1aeebf7d6 ``` > Now login to the website you want to crawl and keep the browser open. **step 3) Now use the websocket url with katana to connect to the active browser session and crawl the website** ```console katana -headless -u https://tesla.com -cwu ws://127.0.0.1:9222/devtools/browser/c5316c9c-19d6-42dc-847a-41d1aeebf7d6 -no-incognito ``` > **Note**: you can use `-cdd` option to specify custom chrome data directory to store browser data and cookies but that does not save session data if cookie is set to `Session` only or expires after certain time. ## Filters *`-field`* ---- Katana comes with built in fields that can be used to filter the output for the desired information, `-f` option can be used to specify any of the available fields. ``` -f, -field string field to display in output (url,path,fqdn,rdn,rurl,qurl,qpath,file,key,value,kv,dir,udir) ``` Here is a table with examples of each field and expected output when used - | FIELD | DESCRIPTION | EXAMPLE | | ------- | --------------------------- | ------------------------------------------------------------ | | `url` | URL Endpoint | `https://admin.projectdiscovery.io/admin/login?user=admin&password=admin` | | `qurl` | URL including query param | `https://admin.projectdiscovery.io/admin/login.php?user=admin&password=admin` | | `qpath` | Path including query param | `/login?user=admin&password=admin` | | `path` | URL Path | `https://admin.projectdiscovery.io/admin/login` | | `fqdn` | Fully Qualified Domain name | `admin.projectdiscovery.io` | | `rdn` | Root Domain name | `projectdiscovery.io` | | `rurl` | Root URL | `https://admin.projectdiscovery.io` | | `ufile` | URL with File | `https://admin.projectdiscovery.io/login.js` | | `file` | Filename in URL | `login.php` | | `key` | Parameter keys in URL | `user,password` | | `value` | Parameter values in URL | `admin,admin` | | `kv` | Keys=Values in URL | `user=admin&password=admin` | | `dir` | URL Directory name | `/admin/` | | `udir` | URL with Directory | `https://admin.projectdiscovery.io/admin/` | Here is an example of using field option to only display all the urls with query parameter in it - ``` katana -u https://tesla.com -f qurl -silent https://shop.tesla.com/en_au?redirect=no https://shop.tesla.com/en_nz?redirect=no https://shop.tesla.com/product/men_s-raven-lightweight-zip-up-bomber-jacket?sku=1740250-00-A https://shop.tesla.com/product/tesla-shop-gift-card?sku=1767247-00-A https://shop.tesla.com/product/men_s-chill-crew-neck-sweatshirt?sku=1740176-00-A https://www.tesla.com/about?redirect=no https://www.tesla.com/about/legal?redirect=no https://www.tesla.com/findus/list?redirect=no ``` ### Custom Fields You can create custom fields to extract and store specific information from page responses using regex rules. These custom fields are defined using a YAML config file and are loaded from the default location at `$HOME/.config/katana/field-config.yaml`. Alternatively, you can use the `-flc` option to load a custom field config file from a different location. Here is example custom field. ```yaml - name: email type: regex regex: - '([a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+\.[a-zA-Z0-9_-]+)' - '([a-zA-Z0-9+._-]+@[a-zA-Z0-9._-]+\.[a-zA-Z0-9_-]+)' - name: phone type: regex regex: - '\d{3}-\d{8}|\d{4}-\d{7}' ``` When defining custom fields, following attributes are supported: - **name** (required) > The value of **name** attribute is used as the `-field` cli option value. - **type** (required) > The type of custom attribute, currently supported option - `regex` - **part** (optional) > The part of the response to extract the information from. The default value is `response`, which includes both the header and body. Other possible values are `header` and `body`. - group (optional) > You can use this attribute to select a specific matched group in regex, for example: `group: 1` #### Running katana using custom field: ```console katana -u https://tesla.com -f email,phone ``` *`-store-field`* --- To compliment `field` option which is useful to filter output at run time, there is `-sf, -store-fields` option which works exactly like field option except instead of filtering, it stores all the information on the disk under `katana_field` directory sorted by target url. Use `-sfd` or `-store-field-dir` to store data in a different location. ``` katana -u https://tesla.com -sf key,fqdn,qurl -silent ``` ```bash $ ls katana_field/ https_www.tesla.com_fqdn.txt https_www.tesla.com_key.txt https_www.tesla.com_qurl.txt ``` The `-store-field` option can be useful for collecting information to build a targeted wordlist for various purposes, including but not limited to: - Identifying the most commonly used parameters - Discovering frequently used paths - Finding commonly used files - Identifying related or unknown subdomains ### Katana Filters *`-extension-match`* --- Crawl output can be easily matched for specific extension using `-em` option to ensure to display only output containing given extension. ``` katana -u https://tesla.com -silent -em js,jsp,json ``` *`-extension-filter`* --- Crawl output can be easily filtered for specific extension using `-ef` option which ensure to remove all the urls containing given extension. ``` katana -u https://tesla.com -silent -ef css,txt,md ``` *`-match-regex`* --- The `-match-regex` or `-mr` flag allows you to filter output URLs using regular expressions. When using this flag, only URLs that match the specified regular expression will be printed in the output. ``` katana -u https://tesla.com -mr 'https://shop\.tesla\.com/*' -silent ``` *`-filter-regex`* --- The `-filter-regex` or `-fr` flag allows you to filter output URLs using regular expressions. When using this flag, it will skip the URLs that are match the specified regular expression. ``` katana -u https://tesla.com -fr 'https://www\.tesla\.com/*' -silent ``` ### Advance Filtering Katana supports DSL-based expressions for advanced matching and filtering capabilities: - To match endpoints with a 200 status code: ```shell katana -u https://www.hackerone.com -mdc 'status_code == 200' ``` - To match endpoints that contain "default" and have a status code other than 403: ```shell katana -u https://www.hackerone.com -mdc 'contains(endpoint, "default") && status_code != 403' ``` - To match endpoints with PHP technologies: ```shell katana -u https://www.hackerone.com -mdc 'contains(to_lower(technologies), "php")' ``` - To filter out endpoints running on Cloudflare: ```shell katana -u https://www.hackerone.com -fdc 'contains(to_lower(technologies), "cloudflare")' ``` DSL functions can be applied to any keys in the jsonl output. For more information on available DSL functions, please visit the [dsl project](https://github.com/projectdiscovery/dsl). Here are additional filter options - ```console katana -h filter Flags: FILTER: -mr, -match-regex string[] regex or list of regex to match on output url (cli, file) -fr, -filter-regex string[] regex or list of regex to filter on output url (cli, file) -f, -field string field to display in output (url,path,fqdn,rdn,rurl,qurl,qpath,file,ufile,key,value,kv,dir,udir) -sf, -store-field string field to store in per-host output (url,path,fqdn,rdn,rurl,qurl,qpath,file,ufile,key,value,kv,dir,udir) -em, -extension-match string[] match output for given extension (eg, -em php,html,js) -ef, -extension-filter string[] filter output for given extension (eg, -ef png,css) -mdc, -match-condition string match response with dsl based condition -fdc, -filter-condition string filter response with dsl based condition ``` ## Rate Limit It's easy to get blocked / banned while crawling if not following target websites limits, katana comes with multiple option to tune the crawl to go as fast / slow we want. *`-delay`* ----- option to introduce a delay in seconds between each new request katana makes while crawling, disabled as default. ``` katana -u https://tesla.com -delay 20 ``` *`-concurrency`* ----- option to control the number of urls per target to fetch at the same time. ``` katana -u https://tesla.com -c 20 ``` *`-parallelism`* ----- option to define number of target to process at same time from list input. ``` katana -u https://tesla.com -p 20 ``` *`-rate-limit`* ----- option to use to define max number of request can go out per second. ``` katana -u https://tesla.com -rl 100 ``` *`-rate-limit-minute`* ----- option to use to define max number of request can go out per minute. ``` katana -u https://tesla.com -rlm 500 ``` Here is all long / short CLI options for rate limit control - ```console katana -h rate-limit Flags: RATE-LIMIT: -c, -concurrency int number of concurrent fetchers to use (default 10) -p, -parallelism int number of concurrent inputs to process (default 10) -rd, -delay int request delay between each request in seconds -rl, -rate-limit int maximum requests to send per second (default 150) -rlm, -rate-limit-minute int maximum number of requests to send per minute ``` ## Output Katana support both file output in plain text format as well as JSON which includes additional information like, `source`, `tag`, and `attribute` name to co-related the discovered endpoint. *`-output`* By default, katana outputs the crawled endpoints in plain text format. The results can be written to a file by using the -output option. ```console katana -u https://example.com -no-scope -output example_endpoints.txt ``` *`-jsonl`* --- ```console katana -u https://example.com -jsonl | jq . ``` ```json { "timestamp": "2023-03-20T16:23:58.027559+05:30", "request": { "method": "GET", "endpoint": "https://example.com", "raw": "GET / HTTP/1.1\r\nHost: example.com\r\nUser-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36\r\nAccept-Encoding: gzip\r\n\r\n" }, "response": { "status_code": 200, "headers": { "accept_ranges": "bytes", "expires": "Mon, 27 Mar 2023 10:53:58 GMT", "last_modified": "Thu, 17 Oct 2019 07:18:26 GMT", "content_type": "text/html; charset=UTF-8", "server": "ECS (dcb/7EA3)", "vary": "Accept-Encoding", "etag": "\"3147526947\"", "cache_control": "max-age=604800", "x_cache": "HIT", "date": "Mon, 20 Mar 2023 10:53:58 GMT", "age": "331239" }, "body": "\n\n\n Example Domain\n\n \n \n \n \n\n\n\n
\n

Example Domain

\n

This domain is for use in illustrative examples in documents. You may use this\n domain in literature without prior coordination or asking for permission.

\n

More information...

\n
\n\n\n", "technologies": [ "Azure", "Amazon ECS", "Amazon Web Services", "Docker", "Azure CDN" ], "raw": "HTTP/1.1 200 OK\r\nContent-Length: 1256\r\nAccept-Ranges: bytes\r\nAge: 331239\r\nCache-Control: max-age=604800\r\nContent-Type: text/html; charset=UTF-8\r\nDate: Mon, 20 Mar 2023 10:53:58 GMT\r\nEtag: \"3147526947\"\r\nExpires: Mon, 27 Mar 2023 10:53:58 GMT\r\nLast-Modified: Thu, 17 Oct 2019 07:18:26 GMT\r\nServer: ECS (dcb/7EA3)\r\nVary: Accept-Encoding\r\nX-Cache: HIT\r\n\r\n\n\n\n Example Domain\n\n \n \n \n \n\n\n\n
\n

Example Domain

\n

This domain is for use in illustrative examples in documents. You may use this\n domain in literature without prior coordination or asking for permission.

\n

More information...

\n
\n\n\n" } } ``` *`-store-response`* ---- The `-store-response` option allows for writing all crawled endpoint requests and responses to a text file. When this option is used, text files including the request and response will be written to the **katana_response** directory. If you would like to specify a custom directory, you can use the `-store-response-dir` option. ```console katana -u https://example.com -no-scope -store-response ``` ```bash $ cat katana_response/index.txt katana_response/example.com/327c3fda87ce286848a574982ddd0b7c7487f816.txt https://example.com (200 OK) katana_response/www.iana.org/bfc096e6dd93b993ca8918bf4c08fdc707a70723.txt http://www.iana.org/domains/reserved (200 OK) ``` **Note:** *`-store-response` option is not supported in `-headless` mode.* Here are additional CLI options related to output - ```console katana -h output OUTPUT: -o, -output string file to write output to -sr, -store-response store http requests/responses -srd, -store-response-dir string store http requests/responses to custom directory -j, -json write output in JSONL(ines) format -nc, -no-color disable output content coloring (ANSI escape codes) -silent display output only -v, -verbose display verbose output -version display project version ``` ## Katana as a library `katana` can be used as a library by creating an instance of the `Option` struct and populating it with the same options that would be specified via CLI. Using the options you can create `crawlerOptions` and so standard or hybrid `crawler`. `crawler.Crawl` method should be called to crawl the input. ```go package main import ( "math" "github.com/projectdiscovery/gologger" "github.com/projectdiscovery/katana/pkg/engine/standard" "github.com/projectdiscovery/katana/pkg/output" "github.com/projectdiscovery/katana/pkg/types" ) func main() { options := &types.Options{ MaxDepth: 3, // Maximum depth to crawl FieldScope: "rdn", // Crawling Scope Field BodyReadSize: math.MaxInt, // Maximum response size to read Timeout: 10, // Timeout is the time to wait for request in seconds Concurrency: 10, // Concurrency is the number of concurrent crawling goroutines Parallelism: 10, // Parallelism is the number of urls processing goroutines Delay: 0, // Delay is the delay between each crawl requests in seconds RateLimit: 150, // Maximum requests to send per second Strategy: "depth-first", // Visit strategy (depth-first, breadth-first) OnResult: func(result output.Result) { // Callback function to execute for result gologger.Info().Msg(result.Request.URL) }, } crawlerOptions, err := types.NewCrawlerOptions(options) if err != nil { gologger.Fatal().Msg(err.Error()) } defer crawlerOptions.Close() crawler, err := standard.New(crawlerOptions) if err != nil { gologger.Fatal().Msg(err.Error()) } defer crawler.Close() var input = "https://www.hackerone.com" err = crawler.Crawl(input) if err != nil { gologger.Warning().Msgf("Could not crawl %s: %s", input, err.Error()) } } ``` --------
katana is made with ❤️ by the [projectdiscovery](https://projectdiscovery.io) team and distributed under [MIT License](LICENSE.md). Join Discord
## /SECURITY.md # Security Policy ## Reporting a Vulnerability DO NOT CREATE AN ISSUE to report a security problem. Instead, please send an email to security@projectdiscovery.io, and we will acknowledge it within 3 working days. ## /cmd/functional-test/main.go ```go path="/cmd/functional-test/main.go" package main import ( "flag" "fmt" "log" "os" "strings" "github.com/logrusorgru/aurora" "github.com/pkg/errors" "github.com/projectdiscovery/katana/internal/testutils" ) var ( debug = os.Getenv("DEBUG") == "true" success = aurora.Green("[✓]").String() failed = aurora.Red("[✘]").String() errored = false devKatanaBinary = flag.String("dev", "", "Dev Branch Katana Binary") ) func main() { flag.Parse() if err := runFunctionalTests(); err != nil { log.Fatalf("Could not run functional tests: %s\n", err) } if errored { os.Exit(1) } } func runFunctionalTests() error { for _, testcase := range testutils.TestCases { if err := runIndividualTestCase(testcase); err != nil { errored = true fmt.Fprintf(os.Stderr, "%s Test \"%s\" failed: %s\n", failed, testcase.Name, err) } else { fmt.Printf("%s Test \"%s\" passed!\n", success, testcase.Name) } } return nil } func runIndividualTestCase(testcase testutils.TestCase) error { argsParts := strings.Fields(testcase.Args) devOutput, err := testutils.RunKatanaBinaryAndGetResults(testcase.Target, *devKatanaBinary, debug, argsParts) if err != nil { return errors.Wrap(err, "could not run Katana dev test") } if testcase.CompareFunc != nil { return testcase.CompareFunc(testcase.Target, devOutput) } if !testutils.CompareOutput(devOutput, testcase.Expected) { return errors.Errorf("expected output %s, got %s", testcase.Expected, devOutput) } return nil } ``` ## /cmd/functional-test/run.sh ```sh path="/cmd/functional-test/run.sh" #!/bin/bash # reading os type from arguments CURRENT_OS=$1 if [ "${CURRENT_OS}" == "windows-latest" ];then extension=.exe fi echo "::group::Building functional-test binary" go build -o functional-test$extension echo "::endgroup::" echo "::group::Building katana binary from current branch" go build -o katana_dev$extension ../katana echo "::endgroup::" echo 'Starting katana functional test' ./functional-test$extension -dev ./katana_dev$extension ``` ## /cmd/integration-test/filters.go ```go path="/cmd/integration-test/filters.go" package main import ( "fmt" "os" "os/exec" "strings" ) var filtersTestcases = map[string]TestCase{ "match condition": &matchConditionIntegrationTest{}, "filter condition": &filterConditionIntegrationTest{}, } type matchConditionIntegrationTest struct{} // Execute executes a test case and returns an error if occurred // Execute the docs at ../README.md if the code stops working for integration. func (h *matchConditionIntegrationTest) Execute() error { results, _ := RunKatanaAndGetResults(false, "-u", "scanme.sh", "-match-condition", "status_code == 200 && contains(body, 'ok')", ) if len(results) != 1 { return fmt.Errorf("match condition failed") } return nil } type filterConditionIntegrationTest struct{} // Execute executes a test case and returns an error if occurred // Execute the docs at ../README.md if the code stops working for integration. func (h *filterConditionIntegrationTest) Execute() error { results, _ := RunKatanaAndGetResults(false, "-u", "scanme.sh", "-filter-condition", "status_code == 200 && contains(body, 'ok')", ) if len(results) != 0 { return fmt.Errorf("filter condition failed") } return nil } // ExtraArgs var ExtraDebugArgs = []string{} func RunKatanaAndGetResults(debug bool, extra ...string) ([]string, error) { cmd := exec.Command("./katana") extra = append(extra, ExtraDebugArgs...) cmd.Args = append(cmd.Args, extra...) cmd.Args = append(cmd.Args, "-duc") // disable auto updates if debug { cmd.Args = append(cmd.Args, "-debug") cmd.Stderr = os.Stderr fmt.Println(cmd.String()) } else { cmd.Args = append(cmd.Args, "-silent") } data, err := cmd.Output() if debug { fmt.Println(string(data)) } if len(data) < 1 && err != nil { return nil, fmt.Errorf("%v: %v", err.Error(), string(data)) } var parts []string items := strings.Split(string(data), "\n") for _, i := range items { if i != "" { parts = append(parts, i) } } return parts, nil } ``` ## /cmd/integration-test/integration-test.go ```go path="/cmd/integration-test/integration-test.go" package main import ( "fmt" "os" "strings" "github.com/logrusorgru/aurora" ) type TestCase interface { // Execute executes a test case and returns any errors if occurred Execute() error } var ( debug = os.Getenv("DEBUG") == "true" customTest = os.Getenv("TEST") errored = false success = aurora.Green("[✓]").String() failed = aurora.Red("[✘]").String() tests = map[string]map[string]TestCase{ "code": libraryTestcases, "filters": filtersTestcases, } ) func main() { for name, tests := range tests { fmt.Printf("Running test cases for \"%s\"\n", aurora.Blue(name)) if customTest != "" && !strings.Contains(name, customTest) { continue // only run tests user asked } for name, test := range tests { err := test.Execute() if err != nil { fmt.Fprintf(os.Stderr, "%s Test \"%s\" failed: %s\n", failed, name, err) errored = true } else { fmt.Printf("%s Test \"%s\" passed!\n", success, name) } } } if errored { os.Exit(1) } } ``` ## /cmd/integration-test/library.go ```go path="/cmd/integration-test/library.go" package main import ( "fmt" "math" "github.com/projectdiscovery/katana/pkg/engine/standard" "github.com/projectdiscovery/katana/pkg/output" "github.com/projectdiscovery/katana/pkg/types" "github.com/projectdiscovery/katana/pkg/utils/queue" ) var libraryTestcases = map[string]TestCase{ "katana as library": &goIntegrationTest{}, } type goIntegrationTest struct{} // Execute executes a test case and returns an error if occurred // Execute the docs at ../README.md if the code stops working for integration. func (h *goIntegrationTest) Execute() error { var crawledURLs []string options := &types.Options{ MaxDepth: 1, FieldScope: "rdn", BodyReadSize: math.MaxInt, RateLimit: 150, Verbose: debug, Strategy: queue.DepthFirst.String(), OnResult: func(r output.Result) { crawledURLs = append(crawledURLs, r.Request.URL) }, } crawlerOptions, err := types.NewCrawlerOptions(options) if err != nil { return err } defer func() { if err := crawlerOptions.Close(); err != nil { fmt.Printf("Error closing crawler options: %v\n", err) } }() crawler, err := standard.New(crawlerOptions) if err != nil { return err } defer func() { if err := crawler.Close(); err != nil { fmt.Printf("Error closing crawler: %v\n", err) } }() var input = "https://public-firing-range.appspot.com" err = crawler.Crawl(input) if err != nil { return err } if len(crawledURLs) == 0 { return fmt.Errorf("no URLs crawled") } return nil } ``` ## /cmd/katana/main.go ```go path="/cmd/katana/main.go" package main import ( "fmt" "os" "os/signal" "path/filepath" "strings" "syscall" "time" "github.com/projectdiscovery/goflags" "github.com/projectdiscovery/gologger" "github.com/projectdiscovery/katana/internal/runner" "github.com/projectdiscovery/katana/pkg/output" "github.com/projectdiscovery/katana/pkg/types" errorutil "github.com/projectdiscovery/utils/errors" fileutil "github.com/projectdiscovery/utils/file" folderutil "github.com/projectdiscovery/utils/folder" pprofutils "github.com/projectdiscovery/utils/pprof" "github.com/rs/xid" ) var ( cfgFile string options = &types.Options{} ) func main() { flagSet, err := readFlags() if err != nil { gologger.Fatal().Msgf("Could not read flags: %s\n", err) } if options.HealthCheck { gologger.Print().Msgf("%s\n", runner.DoHealthCheck(options, flagSet)) os.Exit(0) } katanaRunner, err := runner.New(options) if err != nil || katanaRunner == nil { if options.Version { return } gologger.Fatal().Msgf("could not create runner: %s\n", err) } defer func() { if err := katanaRunner.Close(); err != nil { gologger.Error().Msgf("Error closing katana runner: %v\n", err) } }() // close handler resumeFilename := defaultResumeFilename() go func() { c := make(chan os.Signal, 1) signal.Notify(c, os.Interrupt, syscall.SIGTERM) for range c { gologger.DefaultLogger.Info().Msg("- Ctrl+C pressed in Terminal") if err := katanaRunner.Close(); err != nil { gologger.Error().Msgf("Error closing katana runner: %v\n", err) } gologger.Info().Msgf("Creating resume file: %s\n", resumeFilename) err := katanaRunner.SaveState(resumeFilename) if err != nil { gologger.Error().Msgf("Couldn't create resume file: %s\n", err) } os.Exit(0) } }() var pprofServer *pprofutils.PprofServer if options.PprofServer { pprofServer = pprofutils.NewPprofServer() pprofServer.Start() } if pprofServer != nil { defer pprofServer.Stop() } if err := katanaRunner.ExecuteCrawling(); err != nil { gologger.Fatal().Msgf("could not execute crawling: %s", err) } // on successful execution: // deduplicate the lines in each file in the store-field-dir //use options.StoreFieldDir once https://github.com/projectdiscovery/katana/pull/877 is merged storeFieldDir := "katana_field" _ = folderutil.DedupeLinesInFiles(storeFieldDir) // remove the resume file in case it exists if fileutil.FileExists(resumeFilename) { _ = os.Remove(resumeFilename) } } const defaultBodyReadSize = 4 * 1024 * 1024 func readFlags() (*goflags.FlagSet, error) { flagSet := goflags.NewFlagSet() flagSet.SetDescription(`Katana is a fast crawler focused on execution in automation pipelines offering both headless and non-headless crawling.`) flagSet.CreateGroup("input", "Input", flagSet.StringSliceVarP(&options.URLs, "list", "u", nil, "target url / list to crawl", goflags.FileCommaSeparatedStringSliceOptions), flagSet.StringVar(&options.Resume, "resume", "", "resume scan using resume.cfg"), flagSet.StringSliceVarP(&options.Exclude, "exclude", "e", nil, "exclude host matching specified filter ('cdn', 'private-ips', cidr, ip, regex)", goflags.CommaSeparatedStringSliceOptions), ) flagSet.CreateGroup("config", "Configuration", flagSet.StringSliceVarP(&options.Resolvers, "resolvers", "r", nil, "list of custom resolver (file or comma separated)", goflags.FileCommaSeparatedStringSliceOptions), flagSet.IntVarP(&options.MaxDepth, "depth", "d", 3, "maximum depth to crawl"), flagSet.BoolVarP(&options.ScrapeJSResponses, "js-crawl", "jc", false, "enable endpoint parsing / crawling in javascript file"), flagSet.BoolVarP(&options.ScrapeJSLuiceResponses, "jsluice", "jsl", false, "enable jsluice parsing in javascript file (memory intensive)"), flagSet.DurationVarP(&options.CrawlDuration, "crawl-duration", "ct", 0, "maximum duration to crawl the target for (s, m, h, d) (default s)"), flagSet.StringVarP(&options.KnownFiles, "known-files", "kf", "", "enable crawling of known files (all,robotstxt,sitemapxml), a minimum depth of 3 is required to ensure all known files are properly crawled."), flagSet.IntVarP(&options.BodyReadSize, "max-response-size", "mrs", defaultBodyReadSize, "maximum response size to read"), flagSet.IntVar(&options.Timeout, "timeout", 10, "time to wait for request in seconds"), flagSet.IntVar(&options.TimeStable, "time-stable", 1, "time to wait until the page is stable in seconds"), flagSet.BoolVarP(&options.AutomaticFormFill, "automatic-form-fill", "aff", false, "enable automatic form filling (experimental)"), flagSet.BoolVarP(&options.FormExtraction, "form-extraction", "fx", false, "extract form, input, textarea & select elements in jsonl output"), flagSet.IntVar(&options.Retries, "retry", 1, "number of times to retry the request"), flagSet.StringVar(&options.Proxy, "proxy", "", "http/socks5 proxy to use"), flagSet.BoolVarP(&options.TechDetect, "tech-detect", "td", false, "enable technology detection"), flagSet.StringSliceVarP(&options.CustomHeaders, "headers", "H", nil, "custom header/cookie to include in all http request in header:value format (file)", goflags.FileStringSliceOptions), flagSet.StringVar(&cfgFile, "config", "", "path to the katana configuration file"), flagSet.StringVarP(&options.FormConfig, "form-config", "fc", "", "path to custom form configuration file"), flagSet.StringVarP(&options.FieldConfig, "field-config", "flc", "", "path to custom field configuration file"), flagSet.StringVarP(&options.Strategy, "strategy", "s", "depth-first", "Visit strategy (depth-first, breadth-first)"), flagSet.BoolVarP(&options.IgnoreQueryParams, "ignore-query-params", "iqp", false, "Ignore crawling same path with different query-param values"), flagSet.BoolVarP(&options.TlsImpersonate, "tls-impersonate", "tlsi", false, "enable experimental client hello (ja3) tls randomization"), flagSet.BoolVarP(&options.DisableRedirects, "disable-redirects", "dr", false, "disable following redirects (default false)"), ) flagSet.CreateGroup("debug", "Debug", flagSet.BoolVarP(&options.HealthCheck, "hc", "health-check", false, "run diagnostic check up"), flagSet.StringVarP(&options.ErrorLogFile, "error-log", "elog", "", "file to write sent requests error log"), flagSet.BoolVar(&options.PprofServer, "pprof-server", false, "enable pprof server"), ) flagSet.CreateGroup("headless", "Headless", flagSet.BoolVarP(&options.Headless, "headless", "hl", false, "enable headless hybrid crawling (experimental)"), flagSet.BoolVarP(&options.UseInstalledChrome, "system-chrome", "sc", false, "use local installed chrome browser instead of katana installed"), flagSet.BoolVarP(&options.ShowBrowser, "show-browser", "sb", false, "show the browser on the screen with headless mode"), flagSet.StringSliceVarP(&options.HeadlessOptionalArguments, "headless-options", "ho", nil, "start headless chrome with additional options", goflags.FileCommaSeparatedStringSliceOptions), flagSet.BoolVarP(&options.HeadlessNoSandbox, "no-sandbox", "nos", false, "start headless chrome in --no-sandbox mode"), flagSet.StringVarP(&options.ChromeDataDir, "chrome-data-dir", "cdd", "", "path to store chrome browser data"), flagSet.StringVarP(&options.SystemChromePath, "system-chrome-path", "scp", "", "use specified chrome browser for headless crawling"), flagSet.BoolVarP(&options.HeadlessNoIncognito, "no-incognito", "noi", false, "start headless chrome without incognito mode"), flagSet.StringVarP(&options.ChromeWSUrl, "chrome-ws-url", "cwu", "", "use chrome browser instance launched elsewhere with the debugger listening at this URL"), flagSet.BoolVarP(&options.XhrExtraction, "xhr-extraction", "xhr", false, "extract xhr request url,method in jsonl output"), ) flagSet.CreateGroup("scope", "Scope", flagSet.StringSliceVarP(&options.Scope, "crawl-scope", "cs", nil, "in scope url regex to be followed by crawler", goflags.FileCommaSeparatedStringSliceOptions), flagSet.StringSliceVarP(&options.OutOfScope, "crawl-out-scope", "cos", nil, "out of scope url regex to be excluded by crawler", goflags.FileCommaSeparatedStringSliceOptions), flagSet.StringVarP(&options.FieldScope, "field-scope", "fs", "rdn", "pre-defined scope field (dn,rdn,fqdn) or custom regex (e.g., '(company-staging.io|company.com)')"), flagSet.BoolVarP(&options.NoScope, "no-scope", "ns", false, "disables host based default scope"), flagSet.BoolVarP(&options.DisplayOutScope, "display-out-scope", "do", false, "display external endpoint from scoped crawling"), ) availableFields := strings.Join(output.FieldNames, ",") flagSet.CreateGroup("filter", "Filter", flagSet.StringSliceVarP(&options.OutputMatchRegex, "match-regex", "mr", nil, "regex or list of regex to match on output url (cli, file)", goflags.FileStringSliceOptions), flagSet.StringSliceVarP(&options.OutputFilterRegex, "filter-regex", "fr", nil, "regex or list of regex to filter on output url (cli, file)", goflags.FileStringSliceOptions), flagSet.StringVarP(&options.Fields, "field", "f", "", fmt.Sprintf("field to display in output (%s)", availableFields)), flagSet.StringVarP(&options.StoreFields, "store-field", "sf", "", fmt.Sprintf("field to store in per-host output (%s)", availableFields)), flagSet.StringSliceVarP(&options.ExtensionsMatch, "extension-match", "em", nil, "match output for given extension (eg, -em php,html,js)", goflags.CommaSeparatedStringSliceOptions), flagSet.StringSliceVarP(&options.ExtensionFilter, "extension-filter", "ef", nil, "filter output for given extension (eg, -ef png,css)", goflags.CommaSeparatedStringSliceOptions), flagSet.StringVarP(&options.OutputMatchCondition, "match-condition", "mdc", "", "match response with dsl based condition"), flagSet.StringVarP(&options.OutputFilterCondition, "filter-condition", "fdc", "", "filter response with dsl based condition"), ) flagSet.CreateGroup("ratelimit", "Rate-Limit", flagSet.IntVarP(&options.Concurrency, "concurrency", "c", 10, "number of concurrent fetchers to use"), flagSet.IntVarP(&options.Parallelism, "parallelism", "p", 10, "number of concurrent inputs to process"), flagSet.IntVarP(&options.Delay, "delay", "rd", 0, "request delay between each request in seconds"), flagSet.IntVarP(&options.RateLimit, "rate-limit", "rl", 150, "maximum requests to send per second"), flagSet.IntVarP(&options.RateLimitMinute, "rate-limit-minute", "rlm", 0, "maximum number of requests to send per minute"), ) flagSet.CreateGroup("update", "Update", flagSet.CallbackVarP(runner.GetUpdateCallback(), "update", "up", "update katana to latest version"), flagSet.BoolVarP(&options.DisableUpdateCheck, "disable-update-check", "duc", false, "disable automatic katana update check"), ) flagSet.CreateGroup("output", "Output", flagSet.StringVarP(&options.OutputFile, "output", "o", "", "file to write output to"), flagSet.BoolVarP(&options.StoreResponse, "store-response", "sr", false, "store http requests/responses"), flagSet.StringVarP(&options.StoreResponseDir, "store-response-dir", "srd", "", "store http requests/responses to custom directory"), flagSet.BoolVarP(&options.NoClobber, "no-clobber", "ncb", false, "do not overwrite output file"), flagSet.StringVarP(&options.StoreFieldDir, "store-field-dir", "sfd", "", "store per-host field to custom directory"), flagSet.BoolVarP(&options.OmitRaw, "omit-raw", "or", false, "omit raw requests/responses from jsonl output"), flagSet.BoolVarP(&options.OmitBody, "omit-body", "ob", false, "omit response body from jsonl output"), flagSet.BoolVarP(&options.JSON, "jsonl", "j", false, "write output in jsonl format"), flagSet.BoolVarP(&options.NoColors, "no-color", "nc", false, "disable output content coloring (ANSI escape codes)"), flagSet.BoolVar(&options.Silent, "silent", false, "display output only"), flagSet.BoolVarP(&options.Verbose, "verbose", "v", false, "display verbose output"), flagSet.BoolVar(&options.Debug, "debug", false, "display debug output"), flagSet.BoolVar(&options.Version, "version", false, "display project version"), ) if err := flagSet.Parse(); err != nil { return nil, errorutil.NewWithErr(err).Msgf("could not parse flags") } if cfgFile != "" { if err := flagSet.MergeConfigFile(cfgFile); err != nil { return nil, errorutil.NewWithErr(err).Msgf("could not read config file") } } cleanupOldResumeFiles() return flagSet, nil } func init() { // show detailed stacktrace in debug mode if os.Getenv("DEBUG") == "true" { errorutil.ShowStackTrace = true } } func defaultResumeFilename() string { homedir, err := os.UserHomeDir() if err != nil { gologger.Fatal().Msgf("could not get home directory: %s", err) } configDir := filepath.Join(homedir, ".config", "katana") return filepath.Join(configDir, fmt.Sprintf("resume-%s.cfg", xid.New().String())) } // cleanupOldResumeFiles cleans up resume files older than 10 days. func cleanupOldResumeFiles() { homedir, err := os.UserHomeDir() if err != nil { gologger.Fatal().Msgf("could not get home directory: %s", err) } root := filepath.Join(homedir, ".config", "katana") filter := fileutil.FileFilters{ OlderThan: 24 * time.Hour * 10, // cleanup on the 10th day Prefix: "resume-", } _ = fileutil.DeleteFilesOlderThan(root, filter) } ``` ## /cmd/tools/crawl-maze-score/main.go ```go path="/cmd/tools/crawl-maze-score/main.go" package main import ( "bufio" "fmt" "log" "math" "os" "strings" "github.com/logrusorgru/aurora" "github.com/projectdiscovery/gologger" urlutil "github.com/projectdiscovery/utils/url" ) // expectedResults is the list of expected endpoints from security-crawl-maze // blueprint directory. // https://github.com/google/security-crawl-maze/blob/master/blueprints/utils/resources/expected-results.json var expectedResults = []string{ "/css/font-face.found", "/headers/content-location.found", "/headers/link.found", "/headers/location.found", "/headers/refresh.found", "/html/doctype.found", "/html/manifest.found", "/html/body/background.found", "/html/body/a/href.found", "/html/body/a/ping.found", "/html/body/audio/src.found", "/html/body/audio/source/src.found", "/html/body/audio/source/srcset1x.found", "/html/body/audio/source/srcset2x.found", "/html/body/applet/archive.found", "/html/body/applet/codebase.found", "/html/body/blockquote/cite.found", "/html/body/embed/src.found", "/html/body/form/action-get.found", "/html/body/form/action-post.found", "/html/body/form/button/formaction.found", "/html/body/frameset/frame/src.found", "/html/body/iframe/src.found", "/html/body/iframe/srcdoc.found", "/html/body/img/dynsrc.found", "/html/body/img/lowsrc.found", "/html/body/img/longdesc.found", "/html/body/img/src-data.found", "/html/body/img/src.found", "/html/body/img/srcset1x.found", "/html/body/img/srcset2x.found", "/html/body/input/src.found", "/html/body/isindex/action.found", "/html/body/map/area/ping.found", "/html/body/object/data.found", "/html/body/object/codebase.found", "/html/body/object/param/value.found", "/html/body/script/src.found", "/html/body/svg/image/xlink.found", "/html/body/svg/script/xlink.found", "/html/body/table/background.found", "/html/body/table/td/background.found", "/html/body/video/src.found", "/html/body/video/track/src.found", "/html/body/video/poster.found", "/html/head/profile.found", "/html/head/base/href.found", "/html/head/comment-conditional.found", "/html/head/import/implementation.found", "/html/head/link/href.found", "/html/head/meta/content-csp.found", "/html/head/meta/content-pinned-websites.found", "/html/head/meta/content-reading-view.found", "/html/head/meta/content-redirect.found", "/html/misc/url/full-url.found", "/html/misc/url/path-relative-url.found", "/html/misc/url/protocol-relative-url.found", "/html/misc/url/root-relative-url.found", "/html/misc/string/dot-dot-slash-prefix.found", "/html/misc/string/dot-slash-prefix.found", "/html/misc/string/url-string.found", "/html/misc/string/string-known-extension.pdf", "/javascript/misc/automatic-post.found", "/javascript/misc/comment.found", "/javascript/misc/string-variable.found", "/javascript/misc/string-concat-variable.found", "/javascript/frameworks/angular/event-handler.found", "/javascript/frameworks/angular/router-outlet.found", "/javascript/frameworks/angularjs/ng-href.found", "/javascript/frameworks/polymer/event-handler.found", "/javascript/frameworks/polymer/polymer-router.found", "/javascript/frameworks/react/route-path.found", "/javascript/frameworks/react/index.html/search.found", "/javascript/interactive/js-delete.found", "/javascript/interactive/js-post.found", "/javascript/interactive/js-post-event-listener.found", "/javascript/interactive/js-put.found", "/javascript/interactive/listener-and-event-attribute-first.found", "/javascript/interactive/listener-and-event-attribute-second.found", "/javascript/interactive/multi-step-request-event-attribute.found", "/test/javascript/interactive/multi-step-request-event-listener-div-dom.found", "/test/javascript/interactive/multi-step-request-event-listener-div.found", "/javascript/interactive/multi-step-request-event-listener-dom.found", "/javascript/interactive/multi-step-request-event-listener.found", "/javascript/interactive/multi-step-request-redefine-event-attribute.found", "/javascript/interactive/multi-step-request-remove-button.found", "/javascript/interactive/multi-step-request-remove-event-listener.found", "/javascript/interactive/two-listeners-first.found", "/javascript/interactive/two-listeners-second.found", "/misc/known-files/robots.txt.found", "/misc/known-files/sitemap.xml.found", } func main() { if err := process(); err != nil { log.Fatalf("%s\n", err) } } var urlTestPrefix = "/test" func process() error { if len(os.Args) < 3 { fmt.Printf("Usage: crawl-maze-score output.txt output_headless.txt") return nil } input := os.Args[1] inputHeadless := os.Args[2] links, err := readFoundLinks(input) if err != nil { return err } linksHeadless, err := readFoundLinks(inputHeadless) if err != nil { return err } linksMap := make(map[string]struct{}) linksHeadlessMap := make(map[string]struct{}) for _, link := range links { linksMap[link] = struct{}{} } for _, link := range linksHeadless { linksHeadlessMap[link] = struct{}{} } matches, matchesHeadless := 0, 0 for _, expected := range expectedResults { expected = urlTestPrefix + expected _, normalOk := linksMap[expected] _, headlessOk := linksHeadlessMap[expected] if normalOk { matches++ } if headlessOk { matchesHeadless++ } fmt.Printf("[%s] [%s] %s\n", colorizeText("standard", normalOk), colorizeText("headless", headlessOk), expected) } fmt.Printf("[info] Total links (%d): Standard=>%d Headless=>%d\n", len(expectedResults), len(links), len(linksHeadless)) fmt.Printf("[info] Total: %d NormalMatches=>%d HeadlessMatches=>%d\n", len(expectedResults), matches, matchesHeadless) fmt.Printf("[info] Score: Normal=>%.2f%% Headless=>%.2f%%\n", math.Round(float64(matches*100/len(expectedResults))), math.Round(float64(matchesHeadless*100/len(expectedResults)))) return nil } func colorizeText(text string, value bool) string { if value { return aurora.Green(text + ":yes").String() } return aurora.Red(text + ":no").String() } func strippedLink(link string) string { parsed, err := urlutil.Parse(link) if err != nil { gologger.Warning().Msgf("failed to parse link while extracting path: %v", err) } return parsed.Path } func readFoundLinks(input string) ([]string, error) { file, err := os.Open(input) if err != nil { return nil, err } defer func() { if err := file.Close(); err != nil { gologger.Error().Msgf("Error closing file: %v\n", err) } }() scanner := bufio.NewScanner(file) var links []string for scanner.Scan() { text := scanner.Text() if text == "" { break } if strings.Contains(text, ".found") { links = append(links, strippedLink(text)) } } return links, nil } ``` ## /go.mod ```mod path="/go.mod" module github.com/projectdiscovery/katana go 1.22.2 toolchain go1.24.2 require ( github.com/BishopFox/jsluice v0.0.0-20240110145140-0ddfab153e06 github.com/PuerkitoBio/goquery v1.8.1 github.com/go-rod/rod v0.114.1 github.com/json-iterator/go v1.1.12 github.com/logrusorgru/aurora v2.0.3+incompatible github.com/lukasbob/srcset v0.0.0-20190730101422-86b742e617f3 github.com/mitchellh/mapstructure v1.5.0 github.com/pkg/errors v0.9.1 github.com/projectdiscovery/dsl v0.4.1 github.com/projectdiscovery/fastdialer v0.4.0 github.com/projectdiscovery/goflags v0.1.74 github.com/projectdiscovery/gologger v1.1.54 github.com/projectdiscovery/hmap v0.0.87 github.com/projectdiscovery/mapcidr v1.1.34 github.com/projectdiscovery/ratelimit v0.0.79 github.com/projectdiscovery/retryablehttp-go v1.0.109 github.com/projectdiscovery/utils v0.4.18 github.com/projectdiscovery/wappalyzergo v0.2.25 github.com/remeh/sizedwaitgroup v1.0.0 github.com/rs/xid v1.5.0 github.com/stretchr/testify v1.10.0 go.uber.org/multierr v1.11.0 golang.org/x/net v0.35.0 gopkg.in/yaml.v3 v3.0.1 ) require ( aead.dev/minisign v0.2.0 // indirect github.com/Knetic/govaluate v3.0.0+incompatible // indirect github.com/Masterminds/semver/v3 v3.2.1 // indirect github.com/Mzack9999/gcache v0.0.0-20230410081825-519e28eab057 // indirect github.com/STARRY-S/zip v0.2.1 // indirect github.com/VividCortex/ewma v1.2.0 // indirect github.com/alecthomas/chroma/v2 v2.14.0 // indirect github.com/andybalholm/brotli v1.1.1 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/bodgit/plumbing v1.3.0 // indirect github.com/bodgit/sevenzip v1.6.0 // indirect github.com/bodgit/windows v1.0.1 // indirect github.com/charmbracelet/glamour v0.8.0 // indirect github.com/charmbracelet/lipgloss v0.13.0 // indirect github.com/charmbracelet/x/ansi v0.3.2 // indirect github.com/cheggaaa/pb/v3 v3.1.4 // indirect github.com/cloudflare/circl v1.3.7 // indirect github.com/ditashi/jsbeautifier-go v0.0.0-20141206144643-2520a8026a9c // indirect github.com/dlclark/regexp2 v1.11.4 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/fatih/color v1.15.0 // indirect github.com/felixge/fgprof v0.9.5 // indirect github.com/gaissmai/bart v0.17.10 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/google/go-github/v30 v30.1.0 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/uuid v1.3.1 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-version v1.6.0 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/hdm/jarm-go v0.0.7 // indirect github.com/kataras/jwt v0.1.8 // indirect github.com/klauspost/compress v1.17.11 // indirect github.com/klauspost/pgzip v1.2.6 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mholt/archives v0.1.0 // indirect github.com/minio/selfupdate v0.6.1-0.20230907112617-f11e74f84ca7 // indirect github.com/muesli/reflow v0.3.0 // indirect github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a // indirect github.com/nwaples/rardecode/v2 v2.0.0-beta.4.0.20241112120701-034e449c6e78 // indirect github.com/pierrec/lz4/v4 v4.1.21 // indirect github.com/projectdiscovery/asnmap v1.1.1 // indirect github.com/projectdiscovery/blackrock v0.0.1 // indirect github.com/projectdiscovery/gostruct v0.0.2 // indirect github.com/projectdiscovery/machineid v0.0.0-20240226150047-2e2c51e35983 // indirect github.com/refraction-networking/utls v1.6.7 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/rogpeppe/go-internal v1.12.0 // indirect github.com/sashabaranov/go-openai v1.37.0 // indirect github.com/shirou/gopsutil/v3 v3.23.7 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect github.com/smacker/go-tree-sitter v0.0.0-20230720070738-0d0a9f78d8f8 // indirect github.com/sorairolake/lzip-go v0.3.5 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect github.com/therootcompany/xz v1.0.1 // indirect github.com/tidwall/btree v1.6.0 // indirect github.com/tidwall/buntdb v1.3.0 // indirect github.com/tidwall/gjson v1.18.0 // indirect github.com/tidwall/grect v0.1.4 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/rtred v0.1.2 // indirect github.com/tidwall/tinyqueue v0.1.1 // indirect github.com/ysmood/fetchup v0.2.3 // indirect github.com/ysmood/got v0.34.1 // indirect github.com/yuin/goldmark v1.7.4 // indirect github.com/yuin/goldmark-emoji v1.0.3 // indirect github.com/zcalusic/sysinfo v1.0.2 // indirect go4.org v0.0.0-20230225012048-214862532bf5 // indirect golang.org/x/oauth2 v0.11.0 // indirect golang.org/x/sync v0.11.0 // indirect golang.org/x/term v0.29.0 // indirect golang.org/x/time v0.5.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.33.0 // indirect ) require ( github.com/Mzack9999/go-http-digest-auth-client v0.6.1-0.20220414142836-eb8883508809 // indirect github.com/akrylysov/pogreb v0.10.1 // indirect github.com/andybalholm/cascadia v1.3.2 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/cnf/structhash v0.0.0-20201127153200-e1b16c1ebc08 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dimchansky/utfbom v1.1.1 // indirect github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect github.com/go-ole/go-ole v1.2.6 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/gorilla/css v1.0.1 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/microcosm-cc/bluemonday v1.0.27 // indirect github.com/miekg/dns v1.1.62 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/projectdiscovery/networkpolicy v0.1.12 github.com/projectdiscovery/retryabledns v1.0.98 // indirect github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d // indirect github.com/syndtr/goleveldb v1.0.0 // indirect github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/numcpus v0.6.1 // indirect github.com/ulikunitz/xz v0.5.12 // indirect github.com/weppos/publicsuffix-go v0.30.1-0.20230422193905-8fecedd899db // indirect github.com/ysmood/goob v0.4.0 // indirect github.com/ysmood/gson v0.7.3 // indirect github.com/ysmood/leakless v0.8.0 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect github.com/zmap/rc2 v0.0.0-20190804163417-abaa70531248 // indirect github.com/zmap/zcrypto v0.0.0-20230422215203-9a665e1e9968 // indirect go.etcd.io/bbolt v1.3.7 // indirect golang.org/x/crypto v0.33.0 // indirect golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect golang.org/x/mod v0.22.0 // indirect golang.org/x/sys v0.30.0 // indirect golang.org/x/text v0.22.0 // indirect golang.org/x/tools v0.29.0 // indirect gopkg.in/djherbis/times.v1 v1.3.0 // indirect gopkg.in/yaml.v2 v2.4.0 ) ``` ## /go.sum ```sum path="/go.sum" aead.dev/minisign v0.2.0 h1:kAWrq/hBRu4AARY6AlciO83xhNnW9UaC8YipS2uhLPk= aead.dev/minisign v0.2.0/go.mod h1:zdq6LdSd9TbuSxchxwhpA9zEb9YXcVGoE8JakuiGaIQ= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BishopFox/jsluice v0.0.0-20240110145140-0ddfab153e06 h1:xa/dJgg1qpWdIyr7tQcTV2TUPgBK/f0TTMLMmD5GqjQ= github.com/BishopFox/jsluice v0.0.0-20240110145140-0ddfab153e06/go.mod h1:ENDk4KXEVPZTZPygQAEWJK0BlyEWAyQZhxwCMc+o6A0= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/Knetic/govaluate v3.0.0+incompatible h1:7o6+MAPhYTCF0+fdvoz1xDedhRb4f6s9Tn1Tt7/WTEg= github.com/Knetic/govaluate v3.0.0+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= github.com/Mzack9999/gcache v0.0.0-20230410081825-519e28eab057 h1:KFac3SiGbId8ub47e7kd2PLZeACxc1LkiiNoDOFRClE= github.com/Mzack9999/gcache v0.0.0-20230410081825-519e28eab057/go.mod h1:iLB2pivrPICvLOuROKmlqURtFIEsoJZaMidQfCG1+D4= github.com/Mzack9999/go-http-digest-auth-client v0.6.1-0.20220414142836-eb8883508809 h1:ZbFL+BDfBqegi+/Ssh7im5+aQfBRx6it+kHnC7jaDU8= github.com/Mzack9999/go-http-digest-auth-client v0.6.1-0.20220414142836-eb8883508809/go.mod h1:upgc3Zs45jBDnBT4tVRgRcgm26ABpaP7MoTSdgysca4= github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM= github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ= github.com/RumbleDiscovery/rumble-tools v0.0.0-20201105153123-f2adbb3244d2/go.mod h1:jD2+mU+E2SZUuAOHZvZj4xP4frlOo+N/YrXDvASFhkE= github.com/STARRY-S/zip v0.2.1 h1:pWBd4tuSGm3wtpoqRZZ2EAwOmcHK6XFf7bU9qcJXyFg= github.com/STARRY-S/zip v0.2.1/go.mod h1:xNvshLODWtC4EJ702g7cTYn13G53o1+X9BWnPFpcWV4= github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow= github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4= github.com/akrylysov/pogreb v0.10.1 h1:FqlR8VR7uCbJdfUob916tPM+idpKgeESDXOA1K0DK4w= github.com/akrylysov/pogreb v0.10.1/go.mod h1:pNs6QmpQ1UlTJKDezuRWmaqkgUE2TuU0YTWyqJZ7+lI= github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE= github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E= github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I= github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss= github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE= github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/bits-and-blooms/bloom/v3 v3.5.0 h1:AKDvi1V3xJCmSR6QhcBfHbCN4Vf8FfxeWkMNQfmAGhY= github.com/bits-and-blooms/bloom/v3 v3.5.0/go.mod h1:Y8vrn7nk1tPIlmLtW2ZPV+W7StdVMor6bC1xgpjMZFs= github.com/bodgit/plumbing v1.3.0 h1:pf9Itz1JOQgn7vEOE7v7nlEfBykYqvUYioC61TwWCFU= github.com/bodgit/plumbing v1.3.0/go.mod h1:JOTb4XiRu5xfnmdnDJo6GmSbSbtSyufrsyZFByMtKEs= github.com/bodgit/sevenzip v1.6.0 h1:a4R0Wu6/P1o1pP/3VV++aEOcyeBxeO/xE2Y9NSTrr6A= github.com/bodgit/sevenzip v1.6.0/go.mod h1:zOBh9nJUof7tcrlqJFv1koWRrhz3LbDbUNngkuZxLMc= github.com/bodgit/windows v1.0.1 h1:tF7K6KOluPYygXa3Z2594zxlkbKPAOvqr97etrGNIz4= github.com/bodgit/windows v1.0.1/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/charmbracelet/glamour v0.8.0 h1:tPrjL3aRcQbn++7t18wOpgLyl8wrOHUEDS7IZ68QtZs= github.com/charmbracelet/glamour v0.8.0/go.mod h1:ViRgmKkf3u5S7uakt2czJ272WSg2ZenlYEZXT2x7Bjw= github.com/charmbracelet/lipgloss v0.13.0 h1:4X3PPeoWEDCMvzDvGmTajSyYPcZM4+y8sCA/SsA3cjw= github.com/charmbracelet/lipgloss v0.13.0/go.mod h1:nw4zy0SBX/F/eAO1cWdcvy6qnkDUxr8Lw7dvFrAIbbY= github.com/charmbracelet/x/ansi v0.3.2 h1:wsEwgAN+C9U06l9dCVMX0/L3x7ptvY1qmjMwyfE6USY= github.com/charmbracelet/x/ansi v0.3.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a h1:G99klV19u0QnhiizODirwVksQB91TJKV/UaTnACcG30= github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= github.com/cheggaaa/pb/v3 v3.1.4 h1:DN8j4TVVdKu3WxVwcRKu0sG00IIU6FewoABZzXbRQeo= github.com/cheggaaa/pb/v3 v3.1.4/go.mod h1:6wVjILNBaXMs8c21qRiaUM8BR82erfgau1DQ4iUXmSA= github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs= github.com/chromedp/chromedp v0.9.2/go.mod h1:LkSXJKONWTCHAfQasKFUZI+mxqS4tZqhmtGzzhLsnLs= github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= github.com/cnf/structhash v0.0.0-20201127153200-e1b16c1ebc08 h1:ox2F0PSMlrAAiAdknSRMDrAr8mfxPCfSZolH+/qQnyQ= github.com/cnf/structhash v0.0.0-20201127153200-e1b16c1ebc08/go.mod h1:pCxVEbcm3AMg7ejXyorUXi6HQCzOIBf7zEDVPtw0/U4= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U= github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE= github.com/ditashi/jsbeautifier-go v0.0.0-20141206144643-2520a8026a9c h1:+Zo5Ca9GH0RoeVZQKzFJcTLoAixx5s5Gq3pTIS+n354= github.com/ditashi/jsbeautifier-go v0.0.0-20141206144643-2520a8026a9c/go.mod h1:HJGU9ULdREjOcVGZVPB5s6zYmHi1RxzT71l2wQyLmnE= github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo= github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 h1:2tV76y6Q9BB+NEBasnqvs7e49aEBFI8ejC89PSnWH+4= github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s= github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= github.com/felixge/fgprof v0.9.5 h1:8+vR6yu2vvSKn08urWyEuxx75NWPEvybbkBirEpsbVY= github.com/felixge/fgprof v0.9.5/go.mod h1:yKl+ERSa++RYOs32d8K6WEXCB4uXdLls4ZaZPpayhMM= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/gaissmai/bart v0.17.10 h1:TY1y++A6N/ESrwRLTRWrnVOrQpZqpOYSVnKMu/FYW6o= github.com/gaissmai/bart v0.17.10/go.mod h1:JCPkH/Xt5bSPCKDc6OpzkhSCeib8BIxu3kthzZwcl6w= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-rod/rod v0.114.1 h1:osBWr88guzTXAIzwJWVmGZe3/utT9+lqKjkGSBsYMxw= github.com/go-rod/rod v0.114.1/go.mod h1:aiedSEFg5DwG/fnNbUOTPMTTWX3MRj6vIs/a684Mthw= github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= github.com/gofrs/uuid v3.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-github/v30 v30.1.0 h1:VLDx+UolQICEOKu2m4uAoMti1SxuEBAl7RSEG16L+Oo= github.com/google/go-github/v30 v30.1.0/go.mod h1:n8jBpHl45a/rlBUtRJMOG4GhNADUQFEufcolZ95JfU8= github.com/google/go-github/v50 v50.1.0/go.mod h1:Ev4Tre8QoKiolvbpOSG3FIi4Mlon3S2Nt9W5JYqKiwA= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7 h1:y3N7Bm7Y9/CtpiVkw/ZWj6lSlDF3F74SfKwfTCer72Q= github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hdm/jarm-go v0.0.7 h1:Eq0geenHrBSYuKrdVhrBdMMzOmA+CAMLzN2WrF3eL6A= github.com/hdm/jarm-go v0.0.7/go.mod h1:kinGoS0+Sdn1Rr54OtanET5E5n7AlD6T6CrJAKDjJSQ= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kataras/jwt v0.1.8 h1:u71baOsYD22HWeSOg32tCHbczPjdCk7V4MMeJqTtmGk= github.com/kataras/jwt v0.1.8/go.mod h1:Q5j2IkcIHnfwy+oNY3TVWuEBJNw0ADgCcXK9CaZwV4o= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs= github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8= github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/lukasbob/srcset v0.0.0-20190730101422-86b742e617f3 h1:l1rIRmxNhzeQM+qA3D0CsDLo0Hx45q9JmK0BlCjt6Ks= github.com/lukasbob/srcset v0.0.0-20190730101422-86b742e617f3/go.mod h1:j16TYl5p17+vBMyaL6Nu4ojlOnfX8lc2k2cfmw6m5TQ= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mholt/archives v0.1.0 h1:FacgJyrjiuyomTuNA92X5GyRBRZjE43Y/lrzKIlF35Q= github.com/mholt/archives v0.1.0/go.mod h1:j/Ire/jm42GN7h90F5kzj6hf6ZFzEH66de+hmjEKu+I= github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= github.com/miekg/dns v1.1.35/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM= github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ= github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ= github.com/minio/selfupdate v0.6.1-0.20230907112617-f11e74f84ca7 h1:yRZGarbxsRytL6EGgbqK2mCY+Lk5MWKQYKJT2gEglhc= github.com/minio/selfupdate v0.6.1-0.20230907112617-f11e74f84ca7/go.mod h1:bO02GTIPCMQFTEvE5h4DjYB58bCoZ35XLeBf0buTDdM= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/mreiferson/go-httpclient v0.0.0-20160630210159-31f0106b4474/go.mod h1:OQA4XLvDbMgS8P0CevmM4m9Q3Jq4phKUzcocxuGJ5m8= github.com/mreiferson/go-httpclient v0.0.0-20201222173833-5e475fde3a4d/go.mod h1:OQA4XLvDbMgS8P0CevmM4m9Q3Jq4phKUzcocxuGJ5m8= github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a h1:2MaM6YC3mGu54x+RKAA6JiFFHlHDY1UbkxqppT7wYOg= github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a/go.mod h1:hxSnBBYLK21Vtq/PHd0S2FYCxBXzBua8ov5s1RobyRQ= github.com/nwaples/rardecode/v2 v2.0.0-beta.4.0.20241112120701-034e449c6e78 h1:MYzLheyVx1tJVDqfu3YnN4jtnyALNzLvwl+f58TcvQY= github.com/nwaples/rardecode/v2 v2.0.0-beta.4.0.20241112120701-034e449c6e78/go.mod h1:yntwv/HfMc/Hbvtq9I19D1n58te3h6KsqCf3GxyfBGY= github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE= github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/projectdiscovery/asnmap v1.1.1 h1:ImJiKIaACOT7HPx4Pabb5dksolzaFYsD1kID2iwsDqI= github.com/projectdiscovery/asnmap v1.1.1/go.mod h1:QT7jt9nQanj+Ucjr9BqGr1Q2veCCKSAVyUzLXfEcQ60= github.com/projectdiscovery/blackrock v0.0.1 h1:lHQqhaaEFjgf5WkuItbpeCZv2DUIE45k0VbGJyft6LQ= github.com/projectdiscovery/blackrock v0.0.1/go.mod h1:ANUtjDfaVrqB453bzToU+YB4cUbvBRpLvEwoWIwlTss= github.com/projectdiscovery/dsl v0.4.1 h1:mhAXcqOkY5hwN/wUJDtR7WFUY5nOALc30UjhxeqndXw= github.com/projectdiscovery/dsl v0.4.1/go.mod h1:e9cuaYNYIJ9u6cFx8aRxU0OO3VcZLBH6nZGBlBWbL9Q= github.com/projectdiscovery/fastdialer v0.4.0 h1:licZKyq+Shd5lLDb8uPd60Jp43K4NFE8cr67XD2eg7w= github.com/projectdiscovery/fastdialer v0.4.0/go.mod h1:Q0YLArvpx9GAfY/NcTPMCA9qZuVOGnuVoNYWzKBwxdQ= github.com/projectdiscovery/goflags v0.1.74 h1:n85uTRj5qMosm0PFBfsvOL24I7TdWRcWq/1GynhXS7c= github.com/projectdiscovery/goflags v0.1.74/go.mod h1:UMc9/7dFz2oln+10tv6cy+7WZKTHf9UGhaNkF95emh4= github.com/projectdiscovery/gologger v1.1.54 h1:WMzvJ8j/4gGfPKpCttSTaYCVDU1MWQSJnk3wU8/U6Ws= github.com/projectdiscovery/gologger v1.1.54/go.mod h1:vza/8pe2OKOt+ujFWncngknad1XWr8EnLKlbcejOyUE= github.com/projectdiscovery/gostruct v0.0.2 h1:s8gP8ApugGM4go1pA+sVlPDXaWqNP5BBDDSv7VEdG1M= github.com/projectdiscovery/gostruct v0.0.2/go.mod h1:H86peL4HKwMXcQQtEa6lmC8FuD9XFt6gkNR0B/Mu5PE= github.com/projectdiscovery/hmap v0.0.87 h1:bSIqggL878qmmMG67rNgmEa314GB1o2rFM9wjJbsJHA= github.com/projectdiscovery/hmap v0.0.87/go.mod h1:Je1MuSMaP1gM2toj/t3YQhGQpSJGYjQuQwHJpPyJT6g= github.com/projectdiscovery/machineid v0.0.0-20240226150047-2e2c51e35983 h1:ZScLodGSezQVwsQDtBSMFp72WDq0nNN+KE/5DHKY5QE= github.com/projectdiscovery/machineid v0.0.0-20240226150047-2e2c51e35983/go.mod h1:3G3BRKui7nMuDFAZKR/M2hiOLtaOmyukT20g88qRQjI= github.com/projectdiscovery/mapcidr v1.1.34 h1:udr83vQ7oz3kEOwlsU6NC6o08leJzSDQtls1wmXN/kM= github.com/projectdiscovery/mapcidr v1.1.34/go.mod h1:1+1R6OkKSAKtWDXE9RvxXtXPoajXTYX0eiEdkqlhQqQ= github.com/projectdiscovery/networkpolicy v0.1.12 h1:SwfCOm772jmkLQNKWKZHIhjJK3eYz4RVzMHZJfwtti8= github.com/projectdiscovery/networkpolicy v0.1.12/go.mod h1:8fm26WaxgfNY3CGQWzohQy95oSzZlgikU9Oxd1Pq5mk= github.com/projectdiscovery/ratelimit v0.0.79 h1:9Kzff7K5ZyAX0IWspx5X3fHtff0/TzFq7jDxEScO1Qw= github.com/projectdiscovery/ratelimit v0.0.79/go.mod h1:z+hNaODlTmdajGj7V2yIqcQhB7fovdMSK2PNwpbrlHY= github.com/projectdiscovery/retryabledns v1.0.98 h1:2rz0dExX6pJlp8BrF0ZwwimO+Y6T7KCDsstmUioF8cA= github.com/projectdiscovery/retryabledns v1.0.98/go.mod h1:AeFHeqjpm375uKHKf9dn4+EvwsE/xXGGDU5cT5EEiqQ= github.com/projectdiscovery/retryablehttp-go v1.0.109 h1:z7USVuroBrJJH/ozGS4m+evwukFyJvIvjmvaTNXrhr8= github.com/projectdiscovery/retryablehttp-go v1.0.109/go.mod h1:0A6WpqP585LzGIFHQButwfQin4746gvNK2BrGpmRoXI= github.com/projectdiscovery/utils v0.4.18 h1:cSjMOLXI5gAajfA6KV+0iQG4dGx2IHWLQyND/Snvw7k= github.com/projectdiscovery/utils v0.4.18/go.mod h1:y5gnpQn802iEWqf0djTRNskJlS62P5eqe1VS1+ah0tk= github.com/projectdiscovery/wappalyzergo v0.2.25 h1:K56XmuMrEBowlu2WqSFJDkUju8DBACRKDJ8JUQrqpDk= github.com/projectdiscovery/wappalyzergo v0.2.25/go.mod h1:F8X79ljvmvrG+EIxdxWS9VbdkVTsQupHYz+kXlp8O0o= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/refraction-networking/utls v1.6.7 h1:zVJ7sP1dJx/WtVuITug3qYUq034cDq9B2MR1K67ULZM= github.com/refraction-networking/utls v1.6.7/go.mod h1:BC3O4vQzye5hqpmDTWUqi4P5DDhzJfkV1tdqtawQIH0= github.com/remeh/sizedwaitgroup v1.0.0 h1:VNGGFwNo/R5+MJBf6yrsr110p0m4/OX4S3DCy7Kyl5E= github.com/remeh/sizedwaitgroup v1.0.0/go.mod h1:3j2R4OIe/SeS6YDhICBy22RWjJC5eNCJ1V+9+NVNYlo= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk= github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d h1:hrujxIzL1woJ7AwssoOcM/tq5JjjG2yYOc8odClEiXA= github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU= github.com/sashabaranov/go-openai v1.37.0 h1:hQQowgYm4OXJ1Z/wTrE+XZaO20BYsL0R3uRPSpfNZkY= github.com/sashabaranov/go-openai v1.37.0/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= github.com/shirou/gopsutil/v3 v3.23.7 h1:C+fHO8hfIppoJ1WdsVm1RoI0RwXoNdfTK7yWXV0wVj4= github.com/shirou/gopsutil/v3 v3.23.7/go.mod h1:c4gnmoRC0hQuaLqvxnx1//VXQ0Ms/X9UnJF8pddY5z4= github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/smacker/go-tree-sitter v0.0.0-20230720070738-0d0a9f78d8f8 h1:DxgjlvWYsb80WEN2Zv3WqJFAg2DKjUQJO6URGdf1x6Y= github.com/smacker/go-tree-sitter v0.0.0-20230720070738-0d0a9f78d8f8/go.mod h1:q99oHDsbP0xRwmn7Vmob8gbSMNyvJ83OauXPSuHQuKE= github.com/sorairolake/lzip-go v0.3.5 h1:ms5Xri9o1JBIWvOFAorYtUNik6HI3HgBTkISiqu0Cwg= github.com/sorairolake/lzip-go v0.3.5/go.mod h1:N0KYq5iWrMXI0ZEXKXaS9hCyOjZUQdBDEIbXfoUwbdk= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.4/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= github.com/therootcompany/xz v1.0.1 h1:CmOtsn1CbtmyYiusbfmhmkpAAETj0wBIH6kCYaX+xzw= github.com/therootcompany/xz v1.0.1/go.mod h1:3K3UH1yCKgBneZYhuQUvJ9HPD19UEXEI0BWbMn8qNMY= github.com/tidwall/assert v0.1.0 h1:aWcKyRBUAdLoVebxo95N7+YZVTFF/ASTr7BN4sLP6XI= github.com/tidwall/assert v0.1.0/go.mod h1:QLYtGyeqse53vuELQheYl9dngGCJQ+mTtlxcktb+Kj8= github.com/tidwall/btree v1.6.0 h1:LDZfKfQIBHGHWSwckhXI0RPSXzlo+KYdjK7FWSqOzzg= github.com/tidwall/btree v1.6.0/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EUQ2cKY= github.com/tidwall/buntdb v1.3.0 h1:gdhWO+/YwoB2qZMeAU9JcWWsHSYU3OvcieYgFRS0zwA= github.com/tidwall/buntdb v1.3.0/go.mod h1:lZZrZUWzlyDJKlLQ6DKAy53LnG7m5kHyrEHvvcDmBpU= github.com/tidwall/gjson v1.12.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/grect v0.1.4 h1:dA3oIgNgWdSspFzn1kS4S/RDpZFLrIxAZOdJKjYapOg= github.com/tidwall/grect v0.1.4/go.mod h1:9FBsaYRaR0Tcy4UwefBX/UDcDcDy9V5jUcxHzv2jd5Q= github.com/tidwall/lotsa v1.0.2 h1:dNVBH5MErdaQ/xd9s769R31/n2dXavsQ0Yf4TMEHHw8= github.com/tidwall/lotsa v1.0.2/go.mod h1:X6NiU+4yHA3fE3Puvpnn1XMDrFZrE9JO2/w+UMuqgR8= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/rtred v0.1.2 h1:exmoQtOLvDoO8ud++6LwVsAMTu0KPzLTUrMln8u1yu8= github.com/tidwall/rtred v0.1.2/go.mod h1:hd69WNXQ5RP9vHd7dqekAz+RIdtfBogmglkZSRxCHFQ= github.com/tidwall/tinyqueue v0.1.1 h1:SpNEvEggbpyN5DIReaJ2/1ndroY8iyEGxPYxoSaymYE= github.com/tidwall/tinyqueue v0.1.1/go.mod h1:O/QNHwrnjqr6IHItYrzoHAKYhBkLI67Q096fQP5zMYw= github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI= github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= github.com/tklauser/numcpus v0.6.0/go.mod h1:FEZLMke0lhOUG6w2JadTzp0a+Nl8PF/GFkQ5UVIcaL4= github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc= github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/weppos/publicsuffix-go v0.13.0/go.mod h1:z3LCPQ38eedDQSwmsSRW4Y7t2L8Ln16JPQ02lHAdn5k= github.com/weppos/publicsuffix-go v0.30.1-0.20230422193905-8fecedd899db h1:/WcxBne+5CbtbgWd/sV2wbravmr4sT7y52ifQaCgoLs= github.com/weppos/publicsuffix-go v0.30.1-0.20230422193905-8fecedd899db/go.mod h1:aiQaH1XpzIfgrJq3S1iw7w+3EDbRP7mF5fmwUhWyRUs= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yl2chen/cidranger v1.0.2 h1:lbOWZVCG1tCRX4u24kuM1Tb4nHqWkDxwLdoS+SevawU= github.com/yl2chen/cidranger v1.0.2/go.mod h1:9U1yz7WPYDwf0vpNWFaeRh0bjwz5RVgRy/9UEQfHl0g= github.com/ysmood/fetchup v0.2.3 h1:ulX+SonA0Vma5zUFXtv52Kzip/xe7aj4vqT5AJwQ+ZQ= github.com/ysmood/fetchup v0.2.3/go.mod h1:xhibcRKziSvol0H1/pj33dnKrYyI2ebIvz5cOOkYGns= github.com/ysmood/goob v0.4.0 h1:HsxXhyLBeGzWXnqVKtmT9qM7EuVs/XOgkX7T6r1o1AQ= github.com/ysmood/goob v0.4.0/go.mod h1:u6yx7ZhS4Exf2MwciFr6nIM8knHQIE22lFpWHnfql18= github.com/ysmood/gop v0.0.2 h1:VuWweTmXK+zedLqYufJdh3PlxDNBOfFHjIZlPT2T5nw= github.com/ysmood/gop v0.0.2/go.mod h1:rr5z2z27oGEbyB787hpEcx4ab8cCiPnKxn0SUHt6xzk= github.com/ysmood/got v0.34.1 h1:IrV2uWLs45VXNvZqhJ6g2nIhY+pgIG1CUoOcqfXFl1s= github.com/ysmood/got v0.34.1/go.mod h1:yddyjq/PmAf08RMLSwDjPyCvHvYed+WjHnQxpH851LM= github.com/ysmood/gotrace v0.6.0 h1:SyI1d4jclswLhg7SWTL6os3L1WOKeNn/ZtzVQF8QmdY= github.com/ysmood/gotrace v0.6.0/go.mod h1:TzhIG7nHDry5//eYZDYcTzuJLYQIkykJzCRIo4/dzQM= github.com/ysmood/gson v0.7.3 h1:QFkWbTH8MxyUTKPkVWAENJhxqdBa4lYTQWqZCiLG6kE= github.com/ysmood/gson v0.7.3/go.mod h1:3Kzs5zDl21g5F/BlLTNcuAGAYLKt2lV5G8D1zF3RNmg= github.com/ysmood/leakless v0.8.0 h1:BzLrVoiwxikpgEQR0Lk8NyBN5Cit2b1z+u0mgL4ZJak= github.com/ysmood/leakless v0.8.0/go.mod h1:R8iAXPRaG97QJwqxs74RdwzcRHT1SWCGTNqY8q0JvMQ= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= github.com/yuin/goldmark v1.7.4 h1:BDXOHExt+A7gwPCJgPIIq7ENvceR7we7rOS9TNoLZeg= github.com/yuin/goldmark v1.7.4/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= github.com/yuin/goldmark-emoji v1.0.3 h1:aLRkLHOuBR2czCY4R8olwMjID+tENfhyFDMCRhbIQY4= github.com/yuin/goldmark-emoji v1.0.3/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U= github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/zcalusic/sysinfo v1.0.2 h1:nwTTo2a+WQ0NXwo0BGRojOJvJ/5XKvQih+2RrtWqfxc= github.com/zcalusic/sysinfo v1.0.2/go.mod h1:kluzTYflRWo6/tXVMJPdEjShsbPpsFRyy+p1mBQPC30= github.com/zmap/rc2 v0.0.0-20131011165748-24b9757f5521/go.mod h1:3YZ9o3WnatTIZhuOtot4IcUfzoKVjUHqu6WALIyI0nE= github.com/zmap/rc2 v0.0.0-20190804163417-abaa70531248 h1:Nzukz5fNOBIHOsnP+6I79kPx3QhLv8nBy2mfFhBRq30= github.com/zmap/rc2 v0.0.0-20190804163417-abaa70531248/go.mod h1:3YZ9o3WnatTIZhuOtot4IcUfzoKVjUHqu6WALIyI0nE= github.com/zmap/zcertificate v0.0.0-20180516150559-0e3d58b1bac4/go.mod h1:5iU54tB79AMBcySS0R2XIyZBAVmeHranShAFELYx7is= github.com/zmap/zcertificate v0.0.1/go.mod h1:q0dlN54Jm4NVSSuzisusQY0hqDWvu92C+TWveAxiVWk= github.com/zmap/zcrypto v0.0.0-20201128221613-3719af1573cf/go.mod h1:aPM7r+JOkfL+9qSB4KbYjtoEzJqUK50EXkkJabeNJDQ= github.com/zmap/zcrypto v0.0.0-20201211161100-e54a5822fb7e/go.mod h1:aPM7r+JOkfL+9qSB4KbYjtoEzJqUK50EXkkJabeNJDQ= github.com/zmap/zcrypto v0.0.0-20230422215203-9a665e1e9968 h1:YOQ1vXEwE4Rnj+uQ/3oCuJk5wgVsvUyW+glsndwYuyA= github.com/zmap/zcrypto v0.0.0-20230422215203-9a665e1e9968/go.mod h1:xIuOvYCZX21S5Z9bK1BMrertTGX/F8hgAPw7ERJRNS0= github.com/zmap/zlint/v3 v3.0.0/go.mod h1:paGwFySdHIBEMJ61YjoqT4h7Ge+fdYG4sUQhnTb1lJ8= go.etcd.io/bbolt v1.3.7 h1:j+zJOnnEjF/kyHlDDgGnVL/AIqIJPq8UoB2GSNfkUfQ= go.etcd.io/bbolt v1.3.7/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go4.org v0.0.0-20230225012048-214862532bf5 h1:nifaUDeh+rPaBCMPMQHZmvJf+QdpLFnuQPwx+LxVmtc= go4.org v0.0.0-20230225012048-214862532bf5/go.mod h1:F57wTi5Lrj6WLyswp5EYV1ncrEbFGHD4hhz6S1ZYeaU= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201124201722-c8d3bf9c5392/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20201208171446-5f87f3452ae9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA= golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200528225125-3c3fba18258b/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I= golang.org/x/oauth2 v0.11.0 h1:vPL4xzxBM4niKCW6g9whtaWVXTJf1U5e4aZxxFx/gbU= golang.org/x/oauth2 v0.11.0/go.mod h1:LdF7O/8bLR/qWK9DrpXmbHLTouvRHK0SgJl0GmDBchk= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201126233918-771906719818/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210228012217-479acdf4ea46/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE= golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/djherbis/times.v1 v1.3.0 h1:uxMS4iMtH6Pwsxog094W0FYldiNnfY/xba00vq6C2+o= gopkg.in/djherbis/times.v1 v1.3.0/go.mod h1:AQlg6unIsrsCEdQYhTzERy542dz6SFdQFZFv6mUY0P8= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= ``` ## /integration_tests/run.sh ```sh path="/integration_tests/run.sh" #!/bin/bash echo "::group::Build katana" rm integration-test katana 2>/dev/null cd ../cmd/katana go build mv katana ../../integration_tests/katana echo "::endgroup::" echo "::group::Build katana integration-test" cd ../integration-test go build mv integration-test ../../integration_tests/integration-test cd ../../integration_tests echo "::endgroup::" ./integration-test if [ $? -eq 0 ] then exit 0 else exit 1 fi ``` ## /internal/runner/banner.go ```go path="/internal/runner/banner.go" package runner import ( "github.com/projectdiscovery/gologger" updateutils "github.com/projectdiscovery/utils/update" ) var banner = (` __ __ / /_____ _/ /____ ____ ___ _ / '_/ _ / __/ _ / _ \/ _ / /_/\_\\_,_/\__/\_,_/_//_/\_,_/ `) var version = "v1.1.3" // showBanner is used to show the banner to the user func showBanner() { gologger.Print().Msgf("%s\n", banner) gologger.Print().Msgf("\t\tprojectdiscovery.io\n\n") } // GetUpdateCallback returns a callback function that updates katana func GetUpdateCallback() func() { return func() { showBanner() updateutils.GetUpdateToolCallback("katana", version)() } } ``` ## /internal/runner/executer.go ```go path="/internal/runner/executer.go" package runner import ( "strings" "github.com/projectdiscovery/gologger" errorutil "github.com/projectdiscovery/utils/errors" urlutil "github.com/projectdiscovery/utils/url" "github.com/remeh/sizedwaitgroup" ) // ExecuteCrawling executes the crawling main loop func (r *Runner) ExecuteCrawling() error { if r.crawler == nil { return errorutil.New("crawler is not initialized") } inputs := r.parseInputs() if len(inputs) == 0 { return errorutil.New("no input provided for crawling") } for _, input := range inputs { _ = r.state.InFlightUrls.Set(addSchemeIfNotExists(input), struct{}{}) } defer func() { if err := r.crawler.Close(); err != nil { gologger.Error().Msgf("Error closing crawler: %v\n", err) } }() wg := sizedwaitgroup.New(r.options.Parallelism) for _, input := range inputs { if !r.networkpolicy.Validate(input) { gologger.Info().Msgf("Skipping excluded host %s", input) continue } wg.Add() input = addSchemeIfNotExists(input) go func(input string) { defer wg.Done() if err := r.crawler.Crawl(input); err != nil { gologger.Warning().Msgf("Could not crawl %s: %s", input, err) } r.state.InFlightUrls.Delete(input) }(input) } wg.Wait() return nil } // scheme less urls are skipped and are required for headless mode and other purposes // this method adds scheme if given input does not have any func addSchemeIfNotExists(inputURL string) string { if strings.HasPrefix(inputURL, urlutil.HTTP) || strings.HasPrefix(inputURL, urlutil.HTTPS) { return inputURL } parsed, err := urlutil.Parse(inputURL) if err != nil { gologger.Warning().Msgf("input %v is not a valid url got %v", inputURL, err) return inputURL } if parsed.Port() != "" && (parsed.Port() == "80" || parsed.Port() == "8080") { return urlutil.HTTP + urlutil.SchemeSeparator + inputURL } else { return urlutil.HTTPS + urlutil.SchemeSeparator + inputURL } } ``` ## /internal/runner/healthcheck.go ```go path="/internal/runner/healthcheck.go" package runner import ( "fmt" "net" "os/exec" "runtime" "strings" "github.com/projectdiscovery/goflags" "github.com/projectdiscovery/katana/pkg/types" fileutil "github.com/projectdiscovery/utils/file" permissionutil "github.com/projectdiscovery/utils/permission" ) func DoHealthCheck(options *types.Options, flagSet *goflags.FlagSet) string { // RW permissions on config file cfgFilePath, _ := flagSet.GetConfigFilePath() var test strings.Builder test.WriteString(fmt.Sprintf("Version: %s\n", version)) test.WriteString(fmt.Sprintf("Operative System: %s\n", runtime.GOOS)) test.WriteString(fmt.Sprintf("Architecture: %s\n", runtime.GOARCH)) test.WriteString(fmt.Sprintf("Go Version: %s\n", runtime.Version())) test.WriteString(fmt.Sprintf("Compiler: %s\n", runtime.Compiler)) var testResult string if permissionutil.IsRoot { testResult = "Ok" } else { testResult = "Ko" } test.WriteString(fmt.Sprintf("root: %s\n", testResult)) ok, err := fileutil.IsReadable(cfgFilePath) if ok { testResult = "Ok" } else { testResult = "Ko" } if err != nil { testResult += fmt.Sprintf(" (%s)", err) } test.WriteString(fmt.Sprintf("Config file \"%s\" Read => %s\n", cfgFilePath, testResult)) ok, err = fileutil.IsWriteable(cfgFilePath) if ok { testResult = "Ok" } else { testResult = "Ko" } if err != nil { testResult += fmt.Sprintf(" (%s)", err) } test.WriteString(fmt.Sprintf("Config file \"%s\" Write => %s\n", cfgFilePath, testResult)) c4, err := net.Dial("tcp4", "scanme.sh:80") if err == nil && c4 != nil { _ = c4.Close() } testResult = "Ok" if err != nil { testResult = fmt.Sprintf("Ko (%s)", err) } test.WriteString(fmt.Sprintf("TCP IPv4 connectivity to scanme.sh:80 => %s\n", testResult)) c6, err := net.Dial("tcp6", "scanme.sh:80") if err == nil && c6 != nil { _ = c6.Close() } testResult = "Ok" if err != nil { testResult = fmt.Sprintf("Ko (%s)", err) } test.WriteString(fmt.Sprintf("TCP IPv6 connectivity to scanme.sh:80 => %s\n", testResult)) u4, err := net.Dial("udp4", "scanme.sh:53") if err == nil && u4 != nil { _ = u4.Close() } testResult = "Ok" if err != nil { testResult = fmt.Sprintf("Ko (%s)", err) } test.WriteString(fmt.Sprintf("UDP IPv4 connectivity to scanme.sh:80 => %s\n", testResult)) u6, err := net.Dial("udp6", "scanme.sh:80") if err == nil && u6 != nil { _ = u6.Close() } testResult = "Ok" if err != nil { testResult = fmt.Sprintf("Ko (%s)", err) } test.WriteString(fmt.Sprintf("UDP IPv6 connectivity to scanme.sh:80 => %s\n", testResult)) // attempt to identify if chome is installed locally if chromePath, err := exec.LookPath("chrome"); err == nil { test.WriteString(fmt.Sprintf("Potential chrome binary path (linux/osx) => %s\n", chromePath)) } if chromePath, err := exec.LookPath("chrome.exe"); err == nil { test.WriteString(fmt.Sprintf("Potential chrome.exe binary path (windows) => %s\n", chromePath)) } return test.String() } ``` ## /internal/runner/options.go ```go path="/internal/runner/options.go" package runner import ( "bufio" "os" "path/filepath" "regexp" "strings" "github.com/projectdiscovery/gologger" "github.com/projectdiscovery/gologger/formatter" "github.com/projectdiscovery/katana/pkg/types" "github.com/projectdiscovery/katana/pkg/utils" errorutil "github.com/projectdiscovery/utils/errors" fileutil "github.com/projectdiscovery/utils/file" "gopkg.in/yaml.v3" ) // validateOptions validates the provided options for crawler func validateOptions(options *types.Options) error { if options.MaxDepth <= 0 && options.CrawlDuration.Seconds() <= 0 { return errorutil.New("either max-depth or crawl-duration must be specified") } if len(options.URLs) == 0 && !fileutil.HasStdin() { return errorutil.New("no inputs specified for crawler") } // Disabling automatic form fill (-aff) for headless navigation due to incorrect implementation. // Form filling should be handled via headless actions within the page context if options.Headless && options.AutomaticFormFill { options.AutomaticFormFill = false gologger.Info().Msgf("Automatic form fill (-aff) has been disabled for headless navigation.") } if (options.HeadlessOptionalArguments != nil || options.HeadlessNoSandbox || options.SystemChromePath != "") && !options.Headless { return errorutil.New("headless mode (-hl) is required if -ho, -nos or -scp are set") } if options.SystemChromePath != "" { if !fileutil.FileExists(options.SystemChromePath) { return errorutil.New("specified system chrome binary does not exist") } } if options.StoreResponseDir != "" && !options.StoreResponse { gologger.Debug().Msgf("store response directory specified, enabling \"sr\" flag automatically\n") options.StoreResponse = true } for _, mr := range options.OutputMatchRegex { cr, err := regexp.Compile(mr) if err != nil { return errorutil.NewWithErr(err).Msgf("Invalid value for match regex option") } options.MatchRegex = append(options.MatchRegex, cr) } for _, fr := range options.OutputFilterRegex { cr, err := regexp.Compile(fr) if err != nil { return errorutil.NewWithErr(err).Msgf("Invalid value for filter regex option") } options.FilterRegex = append(options.FilterRegex, cr) } if options.KnownFiles != "" && options.MaxDepth < 3 { gologger.Info().Msgf("Depth automatically set to 3 to accommodate the `--known-files` option (originally set to %d).", options.MaxDepth) options.MaxDepth = 3 } gologger.DefaultLogger.SetFormatter(formatter.NewCLI(options.NoColors)) return nil } // readCustomFormConfig reads custom form fill config func readCustomFormConfig(formConfig string) error { file, err := os.Open(formConfig) if err != nil { return errorutil.NewWithErr(err).Msgf("could not read form config") } defer func() { if err := file.Close(); err != nil { gologger.Error().Msgf("Error closing file: %v\n", err) } }() var data utils.FormFillData if err := yaml.NewDecoder(file).Decode(&data); err != nil { return errorutil.NewWithErr(err).Msgf("could not decode form config") } utils.FormData = data return nil } // parseInputs parses the inputs returning a slice of URLs func (r *Runner) parseInputs() []string { values := make(map[string]struct{}) for _, url := range r.options.URLs { if url == "" { continue } value := normalizeInput(url) if _, ok := values[value]; !ok { values[value] = struct{}{} } } if r.stdin { scanner := bufio.NewScanner(os.Stdin) for scanner.Scan() { value := normalizeInput(scanner.Text()) if _, ok := values[value]; !ok { values[value] = struct{}{} } } } final := make([]string, 0, len(values)) for k := range values { final = append(final, k) } return final } func normalizeInput(value string) string { return strings.TrimSpace(value) } func initExampleFormFillConfig() error { homedir, err := os.UserHomeDir() if err != nil { return errorutil.NewWithErr(err).Msgf("could not get home directory") } defaultConfig := filepath.Join(homedir, ".config", "katana", "form-config.yaml") if fileutil.FileExists(defaultConfig) { return readCustomFormConfig(defaultConfig) } if err := os.MkdirAll(filepath.Dir(defaultConfig), 0775); err != nil { return err } exampleConfig, err := os.Create(defaultConfig) if err != nil { return errorutil.NewWithErr(err).Msgf("could not get home directory") } defer func() { if err := exampleConfig.Close(); err != nil { gologger.Error().Msgf("Error closing example config: %v\n", err) } }() err = yaml.NewEncoder(exampleConfig).Encode(utils.DefaultFormFillData) return err } ``` ## /internal/runner/runner.go ```go path="/internal/runner/runner.go" package runner import ( "encoding/json" "os" "strconv" "github.com/projectdiscovery/gologger" "github.com/projectdiscovery/katana/pkg/engine" "github.com/projectdiscovery/katana/pkg/engine/hybrid" "github.com/projectdiscovery/katana/pkg/engine/parser" "github.com/projectdiscovery/katana/pkg/engine/standard" "github.com/projectdiscovery/katana/pkg/types" "github.com/projectdiscovery/mapcidr" "github.com/projectdiscovery/mapcidr/asn" "github.com/projectdiscovery/networkpolicy" errorutil "github.com/projectdiscovery/utils/errors" fileutil "github.com/projectdiscovery/utils/file" iputil "github.com/projectdiscovery/utils/ip" mapsutil "github.com/projectdiscovery/utils/maps" updateutils "github.com/projectdiscovery/utils/update" "go.uber.org/multierr" ) // Runner creates the required resources for crawling // and executes the crawl process. type Runner struct { crawlerOptions *types.CrawlerOptions stdin bool crawler engine.Engine options *types.Options state *RunnerState networkpolicy *networkpolicy.NetworkPolicy } type RunnerState struct { InFlightUrls *mapsutil.SyncLockMap[string, struct{}] } // New returns a new crawl runner structure func New(options *types.Options) (*Runner, error) { // create the resume configuration structure if options.ShouldResume() { gologger.Info().Msg("Resuming from save checkpoint") file, err := os.ReadFile(options.Resume) if err != nil { return nil, err } runnerState := &RunnerState{} err = json.Unmarshal(file, runnerState) if err != nil { return nil, err } options.URLs = mapsutil.GetKeys(runnerState.InFlightUrls.GetAll()) } options.ConfigureOutput() showBanner() if options.Version { gologger.Info().Msgf("Current version: %s", version) return nil, nil } if !options.DisableUpdateCheck { latestVersion, err := updateutils.GetToolVersionCallback("katana", version)() if err != nil { if options.Verbose { gologger.Error().Msgf("katana version check failed: %v", err.Error()) } } else { gologger.Info().Msgf("Current katana version %v %v", version, updateutils.GetVersionDescription(version, latestVersion)) } } if err := initExampleFormFillConfig(); err != nil { return nil, errorutil.NewWithErr(err).Msgf("could not init default config") } if err := validateOptions(options); err != nil { return nil, errorutil.NewWithErr(err).Msgf("could not validate options") } if options.FormConfig != "" { if err := readCustomFormConfig(options.FormConfig); err != nil { return nil, err } } crawlerOptions, err := types.NewCrawlerOptions(options) if err != nil { return nil, errorutil.NewWithErr(err).Msgf("could not create crawler options") } parser.InitWithOptions(options) var crawler engine.Engine switch { case options.Headless: crawler, err = hybrid.New(crawlerOptions) default: crawler, err = standard.New(crawlerOptions) } if err != nil { return nil, errorutil.NewWithErr(err).Msgf("could not create standard crawler") } var npOptions networkpolicy.Options for _, exclude := range options.Exclude { switch { case exclude == "cdn": //implement cdn check in netoworkpolicy pkg?? continue case exclude == "private-ips": npOptions.DenyList = append(npOptions.DenyList, networkpolicy.DefaultIPv4Denylist...) npOptions.DenyList = append(npOptions.DenyList, networkpolicy.DefaultIPv4DenylistRanges...) npOptions.DenyList = append(npOptions.DenyList, networkpolicy.DefaultIPv6Denylist...) npOptions.DenyList = append(npOptions.DenyList, networkpolicy.DefaultIPv6DenylistRanges...) case iputil.IsCIDR(exclude): npOptions.DenyList = append(npOptions.DenyList, exclude) case asn.IsASN(exclude): // update this to use networkpolicy pkg once https://github.com/projectdiscovery/networkpolicy/pull/55 is merged ips := expandASNInputValue(exclude) npOptions.DenyList = append(npOptions.DenyList, ips...) case iputil.IsPort(exclude): port, _ := strconv.Atoi(exclude) npOptions.DenyPortList = append(npOptions.DenyPortList, port) default: npOptions.DenyList = append(npOptions.DenyList, exclude) } } np, _ := networkpolicy.New(npOptions) runner := &Runner{ options: options, stdin: fileutil.HasStdin(), crawlerOptions: crawlerOptions, crawler: crawler, state: &RunnerState{InFlightUrls: mapsutil.NewSyncLockMap[string, struct{}]()}, networkpolicy: np, } return runner, nil } // Close closes the runner releasing resources func (r *Runner) Close() error { return multierr.Combine( r.crawler.Close(), r.crawlerOptions.Close(), ) } func (r *Runner) SaveState(resumeFilename string) error { runnerState := r.state data, _ := json.Marshal(runnerState) return os.WriteFile(resumeFilename, data, os.ModePerm) } func expandCIDRInputValue(value string) []string { var ips []string ipsCh, _ := mapcidr.IPAddressesAsStream(value) for ip := range ipsCh { ips = append(ips, ip) } return ips } func expandASNInputValue(value string) []string { var ips []string cidrs, _ := asn.GetCIDRsForASNNum(value) for _, cidr := range cidrs { ips = append(ips, expandCIDRInputValue(cidr.String())...) } return ips } ``` ## /internal/testutils/helper.go ```go path="/internal/testutils/helper.go" package testutils func CompareOutput(input, expected []string) bool { if len(input) != len(expected) { return false } for i, v := range input { if v != expected[i] { return false } } return true } ``` ## /internal/testutils/integration.go ```go path="/internal/testutils/integration.go" package testutils import ( "fmt" "os/exec" "strings" ) func RunKatanaBinaryAndGetResults(target string, katanaBinary string, debug bool, args []string) ([]string, error) { cmd := exec.Command("bash", "-c") cmdLine := fmt.Sprintf(`echo %s | %s `, target, katanaBinary) cmdLine += strings.Join(args, " ") cmd.Args = append(cmd.Args, cmdLine) data, err := cmd.Output() if err != nil { return nil, err } parts := []string{} items := strings.Split(string(data), "\n") for _, i := range items { if i != "" { parts = append(parts, i) } } return parts, nil } ``` ## /internal/testutils/testutils.go ```go path="/internal/testutils/testutils.go" package testutils import ( "strings" errorutils "github.com/projectdiscovery/utils/errors" ) type TestCase struct { Name string Target string Args string Expected []string CompareFunc func(target string, got []string) error } var TestCases = []TestCase{ { Name: "Headless Browser Without Incognito", Target: "https://www.hackerone.com/", Expected: nil, Args: "-headless -no-incognito -depth 2 -silent -no-sandbox", CompareFunc: func(target string, got []string) error { for _, res := range got { if strings.Contains(res, target) { return nil } } return errorutils.New("expected %v target in output, but got %v ", target, strings.Join(got, "\n")) }, }, } ``` ## /pkg/engine/common/base.go ```go path="/pkg/engine/common/base.go" package common import ( "bytes" "context" "io" "net/http" "net/url" "time" "github.com/PuerkitoBio/goquery" "github.com/go-rod/rod" "github.com/projectdiscovery/gologger" "github.com/projectdiscovery/katana/pkg/engine/parser" "github.com/projectdiscovery/katana/pkg/engine/parser/files" "github.com/projectdiscovery/katana/pkg/navigation" "github.com/projectdiscovery/katana/pkg/output" "github.com/projectdiscovery/katana/pkg/types" "github.com/projectdiscovery/katana/pkg/utils" "github.com/projectdiscovery/katana/pkg/utils/queue" "github.com/projectdiscovery/retryablehttp-go" errorutil "github.com/projectdiscovery/utils/errors" mapsutil "github.com/projectdiscovery/utils/maps" urlutil "github.com/projectdiscovery/utils/url" "github.com/remeh/sizedwaitgroup" ) type Shared struct { Headers map[string]string KnownFiles *files.KnownFiles Options *types.CrawlerOptions } func NewShared(options *types.CrawlerOptions) (*Shared, error) { shared := &Shared{ Headers: options.Options.ParseCustomHeaders(), Options: options, } if options.Options.KnownFiles != "" { httpclient, _, err := BuildHttpClient(options.Dialer, options.Options, nil) if err != nil { return nil, errorutil.New("could not create http client").Wrap(err) } shared.KnownFiles = files.New(httpclient, options.Options.KnownFiles) } return shared, nil } func (s *Shared) Enqueue(queue *queue.Queue, navigationRequests ...*navigation.Request) { for _, nr := range navigationRequests { if nr.URL == "" || !utils.IsURL(nr.URL) { continue } reqUrl := nr.RequestURL() if s.Options.Options.IgnoreQueryParams { reqUrl = utils.ReplaceAllQueryParam(reqUrl, "") } // Ignore blank URL items and only work on unique items if !s.Options.UniqueFilter.UniqueURL(reqUrl) && len(nr.CustomFields) == 0 { continue } // - URLs stuck in a loop if s.Options.UniqueFilter.IsCycle(nr.RequestURL()) { continue } // skip crawling if the endpoint is not in scope inScope := s.ValidateScope(nr.URL, nr.RootHostname) if !inScope { // if the user requested anyway out of scope items // they are sent to output without visiting if s.Options.Options.DisplayOutScope { s.Output(nr, nil, ErrOutOfScope) } continue } // Skip adding to the crawl queue when the maximum depth is exceeded if nr.Depth > s.Options.Options.MaxDepth { continue } queue.Push(nr, nr.Depth) } } func (s *Shared) ValidateScope(URL string, root string) bool { parsed, err := urlutil.Parse(URL) if err != nil { gologger.Warning().Msgf("failed to parse url while validating scope: %v", err) return false } scopeValidated, err := s.Options.ScopeManager.Validate(parsed.URL, root) return err == nil && scopeValidated } func (s *Shared) Output(navigationRequest *navigation.Request, navigationResponse *navigation.Response, err error) { var errData string if err != nil { errData = err.Error() } // Write the found result to output result := &output.Result{ Timestamp: time.Now(), Request: navigationRequest, Response: navigationResponse, Error: errData, } outputErr := s.Options.OutputWriter.Write(result) if s.Options.Options.OnResult != nil && outputErr == nil { s.Options.Options.OnResult(*result) } } type CrawlSession struct { Ctx context.Context CancelFunc context.CancelFunc URL *url.URL Hostname string Queue *queue.Queue HttpClient *retryablehttp.Client Browser *rod.Browser } func (s *Shared) NewCrawlSessionWithURL(URL string) (*CrawlSession, error) { ctx, cancel := context.WithCancel(context.Background()) if s.Options.Options.CrawlDuration.Seconds() > 0 { //nolint ctx, cancel = context.WithTimeout(ctx, s.Options.Options.CrawlDuration) } parsed, err := urlutil.Parse(URL) if err != nil { cancel() return nil, errorutil.New("could not parse root URL").Wrap(err) } hostname := parsed.Hostname() queue, err := queue.New(s.Options.Options.Strategy, s.Options.Options.Timeout) if err != nil { cancel() return nil, err } queue.Push(&navigation.Request{Method: http.MethodGet, URL: URL, Depth: 0, SkipValidation: true}, 0) if s.KnownFiles != nil { navigationRequests, err := s.KnownFiles.Request(URL) if err != nil { gologger.Warning().Msgf("Could not parse known files for %s: %s\n", URL, err) } s.Enqueue(queue, navigationRequests...) } httpclient, _, err := BuildHttpClient(s.Options.Dialer, s.Options.Options, func(resp *http.Response, depth int) { body, _ := io.ReadAll(resp.Body) reader, _ := goquery.NewDocumentFromReader(bytes.NewReader(body)) var technologyKeys []string if s.Options.Wappalyzer != nil { technologies := s.Options.Wappalyzer.Fingerprint(resp.Header, body) technologyKeys = mapsutil.GetKeys(technologies) } navigationResponse := &navigation.Response{ Depth: depth + 1, RootHostname: hostname, Resp: resp, Body: string(body), Reader: reader, Technologies: technologyKeys, StatusCode: resp.StatusCode, Headers: utils.FlattenHeaders(resp.Header), } navigationRequests := parser.ParseResponse(navigationResponse) s.Enqueue(queue, navigationRequests...) }) if err != nil { cancel() return nil, errorutil.New("could not create http client").Wrap(err) } crawlSession := &CrawlSession{ Ctx: ctx, CancelFunc: cancel, URL: parsed.URL, Hostname: hostname, Queue: queue, HttpClient: httpclient, } return crawlSession, nil } type DoRequestFunc func(crawlSession *CrawlSession, req *navigation.Request) (*navigation.Response, error) func (s *Shared) Do(crawlSession *CrawlSession, doRequest DoRequestFunc) error { wg := sizedwaitgroup.New(s.Options.Options.Concurrency) for item := range crawlSession.Queue.Pop() { if ctxErr := crawlSession.Ctx.Err(); ctxErr != nil { return ctxErr } req, ok := item.(*navigation.Request) if !ok { continue } if !utils.IsURL(req.URL) { gologger.Debug().Msgf("`%v` not a url. skipping", req.URL) continue } if !s.Options.ValidatePath(req.URL) { gologger.Debug().Msgf("`%v` filtered path. skipping", req.URL) continue } inScope, scopeErr := s.Options.ValidateScope(req.URL, crawlSession.Hostname) if scopeErr != nil { gologger.Debug().Msgf("Error validating scope for `%v`: %v. skipping", req.URL, scopeErr) continue } if !req.SkipValidation && !inScope { gologger.Debug().Msgf("`%v` not in scope. skipping", req.URL) continue } wg.Add() // gologger.Debug().Msgf("Visiting: %v", req.URL) // not sure if this is needed go func() { defer wg.Done() s.Options.RateLimit.Take() // Delay if the user has asked for it if s.Options.Options.Delay > 0 { time.Sleep(time.Duration(s.Options.Options.Delay) * time.Second) } resp, err := doRequest(crawlSession, req) if inScope { s.Output(req, resp, err) } if err != nil { gologger.Warning().Msgf("Could not request seed URL %s: %s\n", req.URL, err) outputError := &output.Error{ Timestamp: time.Now(), Endpoint: req.RequestURL(), Source: req.Source, Error: err.Error(), } _ = s.Options.OutputWriter.WriteErr(outputError) return } if resp.Resp == nil || resp.Reader == nil { return } if s.Options.Options.DisableRedirects && resp.IsRedirect() { return } navigationRequests := parser.ParseResponse(resp) s.Enqueue(crawlSession.Queue, navigationRequests...) }() } wg.Wait() return nil } ``` ## /pkg/engine/common/error.go ```go path="/pkg/engine/common/error.go" package common import "errors" var ErrOutOfScope = errors.New("out of scope") ``` ## /pkg/engine/common/http.go ```go path="/pkg/engine/common/http.go" package common import ( "context" "crypto/tls" "net" "net/http" "net/url" "time" "github.com/projectdiscovery/fastdialer/fastdialer" "github.com/projectdiscovery/fastdialer/fastdialer/ja3/impersonate" "github.com/projectdiscovery/katana/pkg/navigation" "github.com/projectdiscovery/katana/pkg/types" "github.com/projectdiscovery/retryablehttp-go" errorutil "github.com/projectdiscovery/utils/errors" proxyutil "github.com/projectdiscovery/utils/proxy" ) type RedirectCallback func(resp *http.Response, depth int) // BuildHttpClient builds a http client based on a profile func BuildHttpClient(dialer *fastdialer.Dialer, options *types.Options, redirectCallback RedirectCallback) (*retryablehttp.Client, *fastdialer.Dialer, error) { // Single Host retryablehttpOptions := retryablehttp.DefaultOptionsSingle retryablehttpOptions.RetryMax = options.Retries transport := &http.Transport{ DialContext: dialer.Dial, DialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) { if options.TlsImpersonate { return dialer.DialTLSWithConfigImpersonate(ctx, network, addr, &tls.Config{InsecureSkipVerify: true, MinVersion: tls.VersionTLS10}, impersonate.Random, nil) } return dialer.DialTLS(ctx, network, addr) }, MaxIdleConns: 100, MaxIdleConnsPerHost: 10, MaxConnsPerHost: 100, TLSClientConfig: &tls.Config{ Renegotiation: tls.RenegotiateOnceAsClient, InsecureSkipVerify: true, }, DisableKeepAlives: false, } // Attempts to overwrite the dial function with the socks proxied version if proxyURL, err := url.Parse(options.Proxy); options.Proxy != "" && err == nil { if ok, err := proxyutil.IsBurp(options.Proxy); err == nil && ok { transport.TLSClientConfig.MaxVersion = tls.VersionTLS12 } transport.Proxy = http.ProxyURL(proxyURL) } client := retryablehttp.NewWithHTTPClient(&http.Client{ Transport: transport, Timeout: time.Duration(options.Timeout) * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { if options.DisableRedirects { return http.ErrUseLastResponse } if len(via) == 10 { return errorutil.New("stopped after 10 redirects") } depth, ok := req.Context().Value(navigation.Depth{}).(int) if !ok { depth = 2 } if redirectCallback != nil { redirectCallback(req.Response, depth) } return nil }, }, retryablehttpOptions) client.CheckRetry = retryablehttp.HostSprayRetryPolicy() return client, dialer, nil } ``` ## /pkg/engine/engine.go ```go path="/pkg/engine/engine.go" package engine type Engine interface { Crawl(string) error Close() error } ``` ## /pkg/engine/hybrid/crawl.go ```go path="/pkg/engine/hybrid/crawl.go" package hybrid import ( "bytes" "io" "net/http" "net/http/httputil" "net/url" "strings" "time" "github.com/PuerkitoBio/goquery" "github.com/go-rod/rod" "github.com/go-rod/rod/lib/proto" "github.com/projectdiscovery/gologger" "github.com/projectdiscovery/katana/pkg/engine/common" "github.com/projectdiscovery/katana/pkg/engine/parser" "github.com/projectdiscovery/katana/pkg/navigation" "github.com/projectdiscovery/katana/pkg/utils" "github.com/projectdiscovery/retryablehttp-go" errorutil "github.com/projectdiscovery/utils/errors" mapsutil "github.com/projectdiscovery/utils/maps" stringsutil "github.com/projectdiscovery/utils/strings" urlutil "github.com/projectdiscovery/utils/url" ) func (c *Crawler) navigateRequest(s *common.CrawlSession, request *navigation.Request) (*navigation.Response, error) { depth := request.Depth + 1 response := &navigation.Response{ Depth: depth, RootHostname: s.Hostname, } page, err := s.Browser.Page(proto.TargetCreateTarget{}) if err != nil { return nil, errorutil.NewWithTag("hybrid", "could not create target").Wrap(err) } defer func() { if err := page.Close(); err != nil { gologger.Error().Msgf("Error closing page: %v\n", err) } }() c.addHeadersToPage(page) pageRouter := NewHijack(page) pageRouter.SetPattern(&proto.FetchRequestPattern{ URLPattern: "*", RequestStage: proto.FetchRequestStageResponse, }) xhrRequests := []navigation.Request{} go pageRouter.Start(func(e *proto.FetchRequestPaused) error { URL, err := urlutil.Parse(e.Request.URL) if err != nil { return errorutil.NewWithTag("hybrid", "could not parse URL").Wrap(err) } body, _ := FetchGetResponseBody(page, e) headers := make(map[string][]string) for _, h := range e.ResponseHeaders { headers[h.Name] = []string{h.Value} } var ( statusCode int statucCodeText string ) if e.ResponseStatusCode != nil { statusCode = *e.ResponseStatusCode } if e.ResponseStatusText != "" { statucCodeText = e.ResponseStatusText } else { statucCodeText = http.StatusText(statusCode) } httpreq, err := http.NewRequest(e.Request.Method, URL.String(), strings.NewReader(e.Request.PostData)) if err != nil { return errorutil.NewWithTag("hybrid", "could not new request").Wrap(err) } // Note: headers are originally sent using `c.addHeadersToPage` below changes are done so that // headers are reflected in request dump // Headers, CustomHeaders, and Cookies are present in e.Request.Headers. We need to consider all of them and not only CustomHeaders // Otherwise, we will miss headers and output will be inconsistent if httpreq != nil { for k, v := range e.Request.Headers { httpreq.Header.Set(k, v.String()) } } httpresp := &http.Response{ Proto: "HTTP/1.1", ProtoMajor: 1, ProtoMinor: 1, StatusCode: statusCode, Status: statucCodeText, Header: headers, Body: io.NopCloser(bytes.NewReader(body)), Request: httpreq, ContentLength: int64(len(body)), } var rawBytesRequest, rawBytesResponse []byte if r, err := retryablehttp.FromRequest(httpreq); err == nil { rawBytesRequest, _ = r.Dump() } else { rawBytesRequest, _ = httputil.DumpRequestOut(httpreq, true) } rawBytesResponse, _ = httputil.DumpResponse(httpresp, true) bodyReader, _ := goquery.NewDocumentFromReader(bytes.NewReader(body)) var technologies map[string]interface{} if c.Options.Wappalyzer != nil { fingerprints := c.Options.Wappalyzer.Fingerprint(headers, body) technologies = make(map[string]interface{}, len(fingerprints)) for k := range fingerprints { technologies[k] = struct{}{} } } resp := &navigation.Response{ Resp: httpresp, Body: string(body), Reader: bodyReader, Depth: depth, RootHostname: s.Hostname, Technologies: mapsutil.GetKeys(technologies), StatusCode: statusCode, Headers: utils.FlattenHeaders(headers), Raw: string(rawBytesResponse), ContentLength: httpresp.ContentLength, } response.ContentLength = resp.ContentLength requestHeaders := make(map[string][]string) for name, value := range e.Request.Headers { requestHeaders[name] = []string{value.Str()} } if e.ResourceType == "XHR" && c.Options.Options.XhrExtraction { xhr := navigation.Request{ URL: httpreq.URL.String(), Method: httpreq.Method, Body: e.Request.PostData, } if len(httpreq.Header) > 0 { xhr.Headers = utils.FlattenHeaders(httpreq.Header) } else { xhr.Headers = utils.FlattenHeaders(requestHeaders) } xhrRequests = append(xhrRequests, xhr) } // trim trailing / normalizedheadlessURL := strings.TrimSuffix(e.Request.URL, "/") matchOriginalURL := stringsutil.EqualFoldAny(request.URL, e.Request.URL, normalizedheadlessURL) if matchOriginalURL { request.Raw = string(rawBytesRequest) response = resp } // process the raw response navigationRequests := parser.ParseResponse(resp) c.Enqueue(s.Queue, navigationRequests...) // do not continue following the request if it's a redirect and redirects are disabled if c.Options.Options.DisableRedirects && resp.IsRedirect() { return nil } return FetchContinueRequest(page, e) })() //nolint defer func() { if err := pageRouter.Stop(); err != nil { gologger.Warning().Msgf("%s\n", err) } }() timeout := time.Duration(c.Options.Options.Timeout) * time.Second page = page.Timeout(timeout) // wait the page to be fully loaded and becoming idle waitNavigation := page.WaitNavigation(proto.PageLifecycleEventNameFirstMeaningfulPaint) err = page.Navigate(request.URL) if err != nil { if c.Options.Options.DisableRedirects && response.IsRedirect() { return response, nil } return nil, errorutil.NewWithTag("hybrid", "could not navigate target").Wrap(err) } waitNavigation() // Wait the page to be stable a duration timeStable := time.Duration(c.Options.Options.TimeStable) * time.Second if timeout < timeStable { gologger.Warning().Msgf("timeout is less than time stable, setting time stable to half of timeout to avoid timeout\n") timeStable = timeout / 2 gologger.Warning().Msgf("setting time stable to %s\n", timeStable) } if err := page.WaitStable(timeStable); err != nil { gologger.Warning().Msgf("could not wait for page to be stable: %s\n", err) } var getDocumentDepth = int(-1) getDocument := &proto.DOMGetDocument{Depth: &getDocumentDepth, Pierce: true} result, err := getDocument.Call(page) if err != nil { return nil, errorutil.NewWithTag("hybrid", "could not get dom").Wrap(err) } var builder strings.Builder traverseDOMNode(result.Root, &builder) body, err := page.HTML() if err != nil { return nil, errorutil.NewWithTag("hybrid", "could not get html").Wrap(err) } parsed, err := urlutil.Parse(request.URL) if err != nil { return nil, errorutil.NewWithTag("hybrid", "url could not be parsed").Wrap(err) } if response.Resp == nil { return nil, errorutil.NewWithTag("hybrid", "response is nil").Wrap(err) } response.Resp.Request.URL = parsed.URL // Create a copy of intrapolated shadow DOM elements and parse them separately responseCopy := *response responseCopy.Body = builder.String() responseCopy.Reader, _ = goquery.NewDocumentFromReader(strings.NewReader(responseCopy.Body)) if responseCopy.Reader != nil { navigationRequests := parser.ParseResponse(&responseCopy) c.Enqueue(s.Queue, navigationRequests...) } response.Body = body response.Reader.Url, _ = url.Parse(request.URL) if c.Options.Options.FormExtraction { response.Forms = append(response.Forms, utils.ParseFormFields(response.Reader)...) } response.Reader, err = goquery.NewDocumentFromReader(strings.NewReader(response.Body)) if err != nil { return nil, errorutil.NewWithTag("hybrid", "could not parse html").Wrap(err) } response.XhrRequests = xhrRequests return response, nil } func (c *Crawler) addHeadersToPage(page *rod.Page) { if len(c.Headers) == 0 { return } var arr []string for k, v := range c.Headers { switch { case stringsutil.EqualFoldAny(k, "User-Agent"): userAgentParams := &proto.NetworkSetUserAgentOverride{ UserAgent: v, } if err := page.SetUserAgent(userAgentParams); err != nil { gologger.Error().Msgf("headless: could not set user agent: %v", err) } default: arr = append(arr, k, v) } } if len(arr) > 0 { _, err := page.SetExtraHeaders(arr) if err != nil { gologger.Error().Msgf("headless: could not set extra headers: %v", err) } } } // traverseDOMNode performs traversal of node completely building a pseudo-HTML // from it including the Shadow DOM, Pseudo elements and other children. // // TODO: Remove this method when we implement human-like browser navigation // which will anyway use browser APIs to find elements instead of goquery // where they will have shadow DOM information. func traverseDOMNode(node *proto.DOMNode, builder *strings.Builder) { buildDOMFromNode(node, builder) if node.TemplateContent != nil { traverseDOMNode(node.TemplateContent, builder) } if node.ContentDocument != nil { traverseDOMNode(node.ContentDocument, builder) } for _, children := range node.Children { traverseDOMNode(children, builder) } for _, shadow := range node.ShadowRoots { traverseDOMNode(shadow, builder) } for _, pseudo := range node.PseudoElements { traverseDOMNode(pseudo, builder) } } const ( elementNode = 1 ) var knownElements = map[string]struct{}{ "a": {}, "applet": {}, "area": {}, "audio": {}, "base": {}, "blockquote": {}, "body": {}, "button": {}, "embed": {}, "form": {}, "frame": {}, "html": {}, "iframe": {}, "img": {}, "import": {}, "input": {}, "isindex": {}, "link": {}, "meta": {}, "object": {}, "script": {}, "svg": {}, "table": {}, "video": {}, } func buildDOMFromNode(node *proto.DOMNode, builder *strings.Builder) { if node.NodeType != elementNode { return } if _, ok := knownElements[node.LocalName]; !ok { return } builder.WriteRune('<') builder.WriteString(node.LocalName) builder.WriteRune(' ') if len(node.Attributes) > 0 { for i := 0; i < len(node.Attributes); i = i + 2 { builder.WriteString(node.Attributes[i]) builder.WriteRune('=') builder.WriteString("\"") builder.WriteString(node.Attributes[i+1]) builder.WriteString("\"") builder.WriteRune(' ') } } builder.WriteRune('>') builder.WriteString("') } ``` ## /pkg/engine/hybrid/doc.go ```go path="/pkg/engine/hybrid/doc.go" // Package hybrid implements the functionality for a hybrid-headless crawler. // It uses both headless browser and net/http for making requests, and goquery for processing rawand dom-rendered web page HTML. package hybrid ``` ## /pkg/engine/hybrid/hijack.go ```go path="/pkg/engine/hybrid/hijack.go" package hybrid import ( "encoding/base64" "github.com/go-rod/rod" "github.com/go-rod/rod/lib/proto" ) // NewHijack create hijack from page. func NewHijack(page *rod.Page) *Hijack { return &Hijack{ page: page, disable: &proto.FetchDisable{}, } } // HijackHandler type type HijackHandler = func(e *proto.FetchRequestPaused) error // Hijack is a hijack handler type Hijack struct { page *rod.Page enable *proto.FetchEnable disable *proto.FetchDisable cancel func() } // SetPattern set pattern directly func (h *Hijack) SetPattern(pattern *proto.FetchRequestPattern) { h.enable = &proto.FetchEnable{ Patterns: []*proto.FetchRequestPattern{pattern}, } } // Start hijack. func (h *Hijack) Start(handler HijackHandler) func() error { if h.enable == nil { panic("hijack pattern not set") } p, cancel := h.page.WithCancel() h.cancel = cancel err := h.enable.Call(p) if err != nil { return func() error { return err } } wait := p.EachEvent(func(e *proto.FetchRequestPaused) { if handler != nil { err = handler(e) } }) return func() error { wait() return err } } // Stop func (h *Hijack) Stop() error { if h.cancel != nil { h.cancel() } return h.disable.Call(h.page) } // FetchGetResponseBody get request body. func FetchGetResponseBody(page *rod.Page, e *proto.FetchRequestPaused) ([]byte, error) { m := proto.FetchGetResponseBody{ RequestID: e.RequestID, } r, err := m.Call(page) if err != nil { return nil, err } if !r.Base64Encoded { return []byte(r.Body), nil } bs, err := base64.StdEncoding.DecodeString(r.Body) if err != nil { return nil, err } return bs, nil } // FetchContinueRequest continue request func FetchContinueRequest(page *rod.Page, e *proto.FetchRequestPaused) error { m := proto.FetchContinueRequest{ RequestID: e.RequestID, } return m.Call(page) } ``` ## /pkg/engine/hybrid/hybrid.go ```go path="/pkg/engine/hybrid/hybrid.go" package hybrid import ( "fmt" "os" "github.com/go-rod/rod" "github.com/go-rod/rod/lib/launcher" "github.com/go-rod/rod/lib/launcher/flags" "github.com/projectdiscovery/gologger" "github.com/projectdiscovery/katana/pkg/engine/common" "github.com/projectdiscovery/katana/pkg/types" errorutil "github.com/projectdiscovery/utils/errors" urlutil "github.com/projectdiscovery/utils/url" ) // Crawler is a standard crawler instance type Crawler struct { *common.Shared browser *rod.Browser // TODO: Remove the Chrome PID kill code in favor of using Leakless(true). // This change will be made if there are no complaints about zombie Chrome processes. // References: // https://github.com/projectdiscovery/katana/issues/632 // https://github.com/projectdiscovery/httpx/issues/1425 // previousPIDs map[int32]struct{} // track already running PIDs tempDir string } // New returns a new standard crawler instance func New(options *types.CrawlerOptions) (*Crawler, error) { var dataStore string var err error if options.Options.ChromeDataDir != "" { dataStore = options.Options.ChromeDataDir } else { dataStore, err = os.MkdirTemp("", "katana-*") if err != nil { return nil, errorutil.NewWithTag("hybrid", "could not create temporary directory").Wrap(err) } } // previousPIDs := processutil.FindProcesses(processutil.IsChromeProcess) var launcherURL string var chromeLauncher *launcher.Launcher if options.Options.ChromeWSUrl != "" { launcherURL = options.Options.ChromeWSUrl } else { // create new chrome launcher instance chromeLauncher, err = buildChromeLauncher(options, dataStore) if err != nil { return nil, err } // launch chrome headless process launcherURL, err = chromeLauncher.Launch() if err != nil { return nil, err } } browser := rod.New().ControlURL(launcherURL) if browserErr := browser.Connect(); browserErr != nil { return nil, errorutil.NewWithErr(browserErr).Msgf("failed to connect to chrome instance at %s", launcherURL) } // create a new browser instance (default to incognito mode) if !options.Options.HeadlessNoIncognito { incognito, err := browser.Incognito() if err != nil { if chromeLauncher != nil { chromeLauncher.Kill() } return nil, errorutil.NewWithErr(err).Msgf("failed to create incognito browser") } browser = incognito } shared, err := common.NewShared(options) if err != nil { return nil, errorutil.NewWithErr(err).WithTag("hybrid") } crawler := &Crawler{ Shared: shared, browser: browser, // previousPIDs: previousPIDs, tempDir: dataStore, } return crawler, nil } // Close closes the crawler process func (c *Crawler) Close() error { if c.Options.Options.ChromeDataDir == "" { if err := os.RemoveAll(c.tempDir); err != nil { return err } } // processutil.CloseProcesses(processutil.IsChromeProcess, c.previousPIDs) return nil } // Crawl crawls a URL with the specified options func (c *Crawler) Crawl(rootURL string) error { crawlSession, err := c.NewCrawlSessionWithURL(rootURL) crawlSession.Browser = c.browser if err != nil { return errorutil.NewWithErr(err).WithTag("hybrid") } defer crawlSession.CancelFunc() gologger.Info().Msgf("Started headless crawling for => %v", rootURL) if err := c.Do(crawlSession, c.navigateRequest); err != nil { return errorutil.NewWithErr(err).WithTag("standard") } return nil } // buildChromeLauncher builds a new chrome launcher instance func buildChromeLauncher(options *types.CrawlerOptions, dataStore string) (*launcher.Launcher, error) { chromeLauncher := launcher.New(). Leakless(true). Set("disable-gpu", "true"). Set("ignore-certificate-errors", "true"). Set("ignore-certificate-errors", "1"). Set("disable-crash-reporter", "true"). Set("disable-notifications", "true"). Set("hide-scrollbars", "true"). Set("window-size", fmt.Sprintf("%d,%d", 1080, 1920)). Set("mute-audio", "true"). Delete("use-mock-keychain"). UserDataDir(dataStore) if options.Options.UseInstalledChrome { if options.Options.SystemChromePath != "" { chromeLauncher.Bin(options.Options.SystemChromePath) } else { if chromePath, hasChrome := launcher.LookPath(); hasChrome { chromeLauncher.Bin(chromePath) } else { return nil, errorutil.NewWithTag("hybrid", "the chrome browser is not installed").WithLevel(errorutil.Fatal) } } } if options.Options.SystemChromePath != "" { chromeLauncher.Bin(options.Options.SystemChromePath) } if options.Options.ShowBrowser { chromeLauncher = chromeLauncher.Headless(false) } else { chromeLauncher = chromeLauncher.Headless(true) } if options.Options.HeadlessNoSandbox { chromeLauncher.Set("no-sandbox", "true") } if options.Options.Proxy != "" && options.Options.Headless { proxyURL, err := urlutil.Parse(options.Options.Proxy) if err != nil { return nil, err } chromeLauncher.Set("proxy-server", proxyURL.String()) } for k, v := range options.Options.ParseHeadlessOptionalArguments() { chromeLauncher.Set(flags.Flag(k), v) } return chromeLauncher, nil } ``` ## /pkg/engine/parser/files/request.go ```go path="/pkg/engine/parser/files/request.go" package files import ( "github.com/projectdiscovery/katana/pkg/navigation" "github.com/projectdiscovery/retryablehttp-go" ) type visitFunc func(URL string) ([]*navigation.Request, error) type KnownFiles struct { parsers []visitFunc httpclient *retryablehttp.Client } // New returns a new known files parser instance func New(httpclient *retryablehttp.Client, files string) *KnownFiles { parser := &KnownFiles{ httpclient: httpclient, } switch files { case "robotstxt": crawler := &robotsTxtCrawler{httpclient: httpclient} parser.parsers = append(parser.parsers, crawler.Visit) case "sitemapxml": crawler := &sitemapXmlCrawler{httpclient: httpclient} parser.parsers = append(parser.parsers, crawler.Visit) default: crawler := &robotsTxtCrawler{httpclient: httpclient} parser.parsers = append(parser.parsers, crawler.Visit) another := &sitemapXmlCrawler{httpclient: httpclient} parser.parsers = append(parser.parsers, another.Visit) } return parser } // Request requests all known files with visitors func (k *KnownFiles) Request(URL string) (navigationRequests []*navigation.Request, err error) { for _, visitor := range k.parsers { navRequests, err := visitor(URL) if err != nil { return navigationRequests, err } navigationRequests = append(navigationRequests, navRequests...) } return } ``` ## /pkg/engine/parser/files/robotstxt.go ```go path="/pkg/engine/parser/files/robotstxt.go" package files import ( "bufio" "fmt" "io" "net/http" "strings" "github.com/projectdiscovery/gologger" "github.com/projectdiscovery/katana/pkg/navigation" "github.com/projectdiscovery/katana/pkg/utils" "github.com/projectdiscovery/retryablehttp-go" errorutil "github.com/projectdiscovery/utils/errors" ) type robotsTxtCrawler struct { httpclient *retryablehttp.Client } // Visit visits the provided URL with file crawlers func (r *robotsTxtCrawler) Visit(URL string) ([]*navigation.Request, error) { URL = strings.TrimSuffix(URL, "/") requestURL := fmt.Sprintf("%s/robots.txt", URL) req, err := retryablehttp.NewRequest(http.MethodGet, requestURL, nil) if err != nil { return nil, errorutil.NewWithTag("robotscrawler", "could not create request").Wrap(err) } req.Header.Set("User-Agent", utils.WebUserAgent()) resp, err := r.httpclient.Do(req) if err != nil { return nil, errorutil.NewWithTag("robotscrawler", "could not do request").Wrap(err) } defer func() { if err := resp.Body.Close(); err != nil { gologger.Error().Msgf("Error closing response body: %v\n", err) } }() return r.parseReader(resp.Body, resp) } func (r *robotsTxtCrawler) parseReader(reader io.Reader, resp *http.Response) (navigationRequests []*navigation.Request, err error) { scanner := bufio.NewScanner(reader) for scanner.Scan() { text := scanner.Text() splitted := strings.SplitN(text, ": ", 2) if len(splitted) < 2 { continue } directive := strings.ToLower(splitted[0]) if strings.HasPrefix(directive, "allow") || strings.EqualFold(directive, "disallow") { navResp := &navigation.Response{ Depth: 2, Resp: resp, StatusCode: resp.StatusCode, Headers: utils.FlattenHeaders(resp.Header), } navRequest := navigation.NewNavigationRequestURLFromResponse(strings.Trim(splitted[1], " "), resp.Request.URL.String(), "file", "robotstxt", navResp) navigationRequests = append(navigationRequests, navRequest) } } return } ``` ## /pkg/engine/parser/files/robotstxt_test.go ```go path="/pkg/engine/parser/files/robotstxt_test.go" package files import ( "net/http" "strings" "testing" urlutil "github.com/projectdiscovery/utils/url" "github.com/stretchr/testify/require" ) func TestRobotsTxtParseReader(t *testing.T) { requests := []string{} crawler := &robotsTxtCrawler{} content := `User-agent: * Disallow: /test/misc/known-files/robots.txt.found User-agent: * Disallow: /test/includes/ # User-agent: Googlebot # Allow: /random/ Sitemap: https://example.com/sitemap.xml` parsed, err := urlutil.Parse("http://localhost/robots.txt") require.Nil(t, err) navigationRequests, err := crawler.parseReader(strings.NewReader(content), &http.Response{Request: &http.Request{URL: parsed.URL}}) require.Nil(t, err) for _, navReq := range navigationRequests { requests = append(requests, navReq.URL) } require.ElementsMatch(t, requests, []string{ "http://localhost/test/includes/", "http://localhost/test/misc/known-files/robots.txt.found", }, "could not get correct elements") } ``` ## /pkg/engine/parser/files/sitemapxml.go ```go path="/pkg/engine/parser/files/sitemapxml.go" package files import ( "encoding/xml" "fmt" "io" "net/http" "strings" "github.com/projectdiscovery/gologger" "github.com/projectdiscovery/katana/pkg/navigation" "github.com/projectdiscovery/katana/pkg/utils" "github.com/projectdiscovery/retryablehttp-go" errorutil "github.com/projectdiscovery/utils/errors" ) type sitemapXmlCrawler struct { httpclient *retryablehttp.Client } // Visit visits the provided URL with file crawlers func (r *sitemapXmlCrawler) Visit(URL string) (navigationRequests []*navigation.Request, err error) { URL = strings.TrimSuffix(URL, "/") requestURL := fmt.Sprintf("%s/sitemap.xml", URL) req, err := retryablehttp.NewRequest(http.MethodGet, requestURL, nil) if err != nil { return nil, errorutil.NewWithTag("sitemapcrawler", "could not create request").Wrap(err) } req.Header.Set("User-Agent", utils.WebUserAgent()) resp, err := r.httpclient.Do(req) if err != nil { return nil, errorutil.NewWithTag("sitemapcrawler", "could not do request").Wrap(err) } defer func() { if err := resp.Body.Close(); err != nil { gologger.Error().Msgf("Error closing response body: %v\n", err) } }() navigationRequests, err = r.parseReader(resp.Body, resp) if err != nil { return nil, errorutil.NewWithTag("sitemapcrawler", "could not parse sitemap").Wrap(err) } return } type sitemapStruct struct { URLs []parsedURL `xml:"url"` Sitemap []parsedURL `xml:"sitemap"` } type parsedURL struct { Loc string `xml:"loc"` } func (r *sitemapXmlCrawler) parseReader(reader io.Reader, resp *http.Response) (navigationRequests []*navigation.Request, err error) { sitemap := sitemapStruct{} if err := xml.NewDecoder(reader).Decode(&sitemap); err != nil { return nil, errorutil.NewWithTag("sitemapcrawler", "could not decode xml").Wrap(err) } for _, url := range sitemap.URLs { navResp := &navigation.Response{ Depth: 2, Resp: resp, StatusCode: resp.StatusCode, Headers: utils.FlattenHeaders(resp.Header), } navigationRequests = append(navigationRequests, navigation.NewNavigationRequestURLFromResponse(strings.Trim(url.Loc, " \t\n"), resp.Request.URL.String(), "file", "sitemapxml", navResp)) } for _, url := range sitemap.Sitemap { navResp := &navigation.Response{ Depth: 2, Resp: resp, StatusCode: resp.StatusCode, Headers: utils.FlattenHeaders(resp.Header), } navigationRequests = append(navigationRequests, navigation.NewNavigationRequestURLFromResponse(strings.Trim(url.Loc, " \t\n"), resp.Request.URL.String(), "file", "sitemapxml", navResp)) } return } ``` ## /pkg/engine/parser/files/sitemapxml_test.go ```go path="/pkg/engine/parser/files/sitemapxml_test.go" package files import ( "net/http" "strings" "testing" urlutil "github.com/projectdiscovery/utils/url" "github.com/stretchr/testify/require" ) func TestSitemapXmlParseReader(t *testing.T) { requests := []string{} crawler := &sitemapXmlCrawler{} content := ` http://security-crawl-maze.app/test/misc/known-files/sitemap.xml.found 2019-06-19T12:00:00+00:00 ` parsed, err := urlutil.Parse("http://security-crawl-maze.app/sitemap.xml") require.Nil(t, err) navigationRequests, err := crawler.parseReader(strings.NewReader(content), &http.Response{Request: &http.Request{URL: parsed.URL}}) require.Nil(t, err) for _, navReq := range navigationRequests { requests = append(requests, navReq.URL) } require.ElementsMatch(t, requests, []string{ "http://security-crawl-maze.app/test/misc/known-files/sitemap.xml.found", }, "could not get correct elements") } ``` ## /pkg/engine/parser/parser.go ```go path="/pkg/engine/parser/parser.go" package parser import ( "mime/multipart" "net/http" "strings" "github.com/PuerkitoBio/goquery" "github.com/projectdiscovery/gologger" "github.com/projectdiscovery/katana/pkg/navigation" "github.com/projectdiscovery/katana/pkg/output" "github.com/projectdiscovery/katana/pkg/utils" stringsutil "github.com/projectdiscovery/utils/strings" urlutil "github.com/projectdiscovery/utils/url" "golang.org/x/net/html" ) // responseParserFunc is a function that parses the document returning // new navigation items or requests for the crawler. type ResponseParserFunc func(resp *navigation.Response) []*navigation.Request type responseParserType int const ( headerParser responseParserType = iota + 1 bodyParser contentParser ) type responseParser struct { parserType responseParserType parserFunc ResponseParserFunc } // responseParsers is a list of response parsers for the standard engine var responseParsers = []responseParser{ // Header based parsers {headerParser, headerContentLocationParser}, {headerParser, headerLinkParser}, {headerParser, headerRefreshParser}, // Body based parsers {bodyParser, bodyATagParser}, {bodyParser, bodyLinkHrefTagParser}, {bodyParser, bodyBackgroundTagParser}, {bodyParser, bodyAudioTagParser}, {bodyParser, bodyAppletTagParser}, {bodyParser, bodyImgTagParser}, {bodyParser, bodyObjectTagParser}, {bodyParser, bodySvgTagParser}, {bodyParser, bodyTableTagParser}, {bodyParser, bodyVideoTagParser}, {bodyParser, bodyButtonFormactionTagParser}, {bodyParser, bodyBlockquoteCiteTagParser}, {bodyParser, bodyFrameSrcTagParser}, {bodyParser, bodyMapAreaPingTagParser}, {bodyParser, bodyBaseHrefTagParser}, {bodyParser, bodyImportImplementationTagParser}, {bodyParser, bodyEmbedTagParser}, {bodyParser, bodyFrameTagParser}, {bodyParser, bodyIframeTagParser}, {bodyParser, bodyInputSrcTagParser}, {bodyParser, bodyIsindexActionTagParser}, {bodyParser, bodyScriptSrcTagParser}, {bodyParser, bodyMetaContentTagParser}, {bodyParser, bodyHtmlManifestTagParser}, {bodyParser, bodyHtmlDoctypeTagParser}, {bodyParser, bodyHtmxAttrParser}, // custom field regex parser {bodyParser, customFieldRegexParser}, } // parseResponse runs the response parsers on the navigation response func ParseResponse(resp *navigation.Response) (navigationRequests []*navigation.Request) { for _, parser := range responseParsers { switch { case parser.parserType == headerParser && resp.Resp != nil: navigationRequests = append(navigationRequests, parser.parserFunc(resp)...) case parser.parserType == bodyParser && resp.Reader != nil: navigationRequests = append(navigationRequests, parser.parserFunc(resp)...) case parser.parserType == contentParser && len(resp.Body) > 0: navigationRequests = append(navigationRequests, parser.parserFunc(resp)...) } } return } // ------------------------------------------------------------------------- // Begin Header based parsers // ------------------------------------------------------------------------- // headerContentLocationParser parsers Content-Location header from response func headerContentLocationParser(resp *navigation.Response) (navigationRequests []*navigation.Request) { header := resp.Resp.Header.Get("Content-Location") if header == "" { return } navigationRequests = append(navigationRequests, navigation.NewNavigationRequestURLFromResponse(header, resp.Resp.Request.URL.String(), "header", "content-location", resp)) return } // headerLinkParser parsers Link header from response func headerLinkParser(resp *navigation.Response) (navigationRequests []*navigation.Request) { header := resp.Resp.Header.Get("Link") if header == "" { return } values := utils.ParseLinkTag(header) for _, value := range values { navigationRequests = append(navigationRequests, navigation.NewNavigationRequestURLFromResponse(value, resp.Resp.Request.URL.String(), "header", "link", resp)) } return } // headerLocationParser parsers Location header from response func headerLocationParser(resp *navigation.Response) (navigationRequests []*navigation.Request) { header := resp.Resp.Header.Get("Location") if header == "" { return } navigationRequests = append(navigationRequests, navigation.NewNavigationRequestURLFromResponse(header, resp.Resp.Request.URL.String(), "header", "location", resp)) return } // headerRefreshParser parsers Refresh header from response func headerRefreshParser(resp *navigation.Response) (navigationRequests []*navigation.Request) { header := resp.Resp.Header.Get("Refresh") if header == "" { return } values := utils.ParseRefreshTag(header) if values == "" { return } navigationRequests = append(navigationRequests, navigation.NewNavigationRequestURLFromResponse(values, resp.Resp.Request.URL.String(), "header", "refresh", resp)) return } // ------------------------------------------------------------------------- // Begin Body based parsers // ------------------------------------------------------------------------- // bodyATagParser parses A tag from response func bodyATagParser(resp *navigation.Response) (navigationRequests []*navigation.Request) { resp.Reader.Find("a").Each(func(i int, item *goquery.Selection) { href, ok := item.Attr("href") if ok && href != "" { navigationRequests = append(navigationRequests, navigation.NewNavigationRequestURLFromResponse(href, resp.Resp.Request.URL.String(), "a", "href", resp)) } ping, ok := item.Attr("ping") if ok && ping != "" { navigationRequests = append(navigationRequests, navigation.NewNavigationRequestURLFromResponse(ping, resp.Resp.Request.URL.String(), "a", "ping", resp)) } }) return } // bodyLinkHrefTagParser parses link tag from response func bodyLinkHrefTagParser(resp *navigation.Response) (navigationRequests []*navigation.Request) { resp.Reader.Find("link[href]").Each(func(i int, item *goquery.Selection) { href, ok := item.Attr("href") if ok && href != "" { navigationRequests = append(navigationRequests, navigation.NewNavigationRequestURLFromResponse(href, resp.Resp.Request.URL.String(), "link", "href", resp)) } }) return } // bodyEmbedTagParser parses Embed tag from response func bodyEmbedTagParser(resp *navigation.Response) (navigationRequests []*navigation.Request) { resp.Reader.Find("embed[src]").Each(func(i int, item *goquery.Selection) { src, ok := item.Attr("src") if ok && src != "" { navigationRequests = append(navigationRequests, navigation.NewNavigationRequestURLFromResponse(src, resp.Resp.Request.URL.String(), "embed", "src", resp)) } }) return } // bodyFrameTagParser parses frame tag from response func bodyFrameTagParser(resp *navigation.Response) (navigationRequests []*navigation.Request) { resp.Reader.Find("frame[src]").Each(func(i int, item *goquery.Selection) { src, ok := item.Attr("src") if ok && src != "" { navigationRequests = append(navigationRequests, navigation.NewNavigationRequestURLFromResponse(src, resp.Resp.Request.URL.String(), "frame", "src", resp)) } }) return } // bodyIframeTagParser parses iframe tag from response func bodyIframeTagParser(resp *navigation.Response) (navigationRequests []*navigation.Request) { resp.Reader.Find("iframe").Each(func(i int, item *goquery.Selection) { src, ok := item.Attr("src") if ok && src != "" { navigationRequests = append(navigationRequests, navigation.NewNavigationRequestURLFromResponse(src, resp.Resp.Request.URL.String(), "iframe", "src", resp)) } srcDoc, ok := item.Attr("srcdoc") if ok && srcDoc != "" { endpoints := utils.ExtractRelativeEndpoints(srcDoc) for _, item := range endpoints { navigationRequests = append(navigationRequests, navigation.NewNavigationRequestURLFromResponse(item, resp.Resp.Request.URL.String(), "iframe", "srcdoc", resp)) } } }) return } // bodyInputSrcTagParser parses input image src tag from response func bodyInputSrcTagParser(resp *navigation.Response) (navigationRequests []*navigation.Request) { resp.Reader.Find("input[type='image' i]").Each(func(i int, item *goquery.Selection) { src, ok := item.Attr("src") if ok && src != "" { navigationRequests = append(navigationRequests, navigation.NewNavigationRequestURLFromResponse(src, resp.Resp.Request.URL.String(), "input-image", "src", resp)) } }) return } // bodyIsindexActionTagParser parses isindex action tag from response func bodyIsindexActionTagParser(resp *navigation.Response) (navigationRequests []*navigation.Request) { resp.Reader.Find("isindex[action]").Each(func(i int, item *goquery.Selection) { src, ok := item.Attr("action") if ok && src != "" { navigationRequests = append(navigationRequests, navigation.NewNavigationRequestURLFromResponse(src, resp.Resp.Request.URL.String(), "isindex", "action", resp)) } }) return } // bodyScriptSrcTagParser parses script src tag from response func bodyScriptSrcTagParser(resp *navigation.Response) (navigationRequests []*navigation.Request) { resp.Reader.Find("script[src]").Each(func(i int, item *goquery.Selection) { src, ok := item.Attr("src") if ok && src != "" { navigationRequests = append(navigationRequests, navigation.NewNavigationRequestURLFromResponse(src, resp.Resp.Request.URL.String(), "script", "src", resp)) } }) return } // bodyBackgroundTagParser parses body background tag from response func bodyBackgroundTagParser(resp *navigation.Response) (navigationRequests []*navigation.Request) { resp.Reader.Find("body[background]").Each(func(i int, item *goquery.Selection) { src, ok := item.Attr("background") if ok && src != "" { navigationRequests = append(navigationRequests, navigation.NewNavigationRequestURLFromResponse(src, resp.Resp.Request.URL.String(), "body", "background", resp)) } }) return } // bodyAudioTagParser parses body audio tag from response func bodyAudioTagParser(resp *navigation.Response) (navigationRequests []*navigation.Request) { resp.Reader.Find("audio").Each(func(i int, item *goquery.Selection) { src, ok := item.Attr("src") if ok && src != "" { navigationRequests = append(navigationRequests, navigation.NewNavigationRequestURLFromResponse(src, resp.Resp.Request.URL.String(), "audio", "src", resp)) } item.Find("source").Each(func(i int, s *goquery.Selection) { src, ok := s.Attr("src") if ok && src != "" { navigationRequests = append(navigationRequests, navigation.NewNavigationRequestURLFromResponse(src, resp.Resp.Request.URL.String(), "audio", "source", resp)) } srcSet, ok := s.Attr("srcset") if ok && srcSet != "" { for _, value := range utils.ParseSRCSetTag(srcSet) { navigationRequests = append(navigationRequests, navigation.NewNavigationRequestURLFromResponse(value, resp.Resp.Request.URL.String(), "audio", "sourcesrcset", resp)) } } }) }) return } // bodyAppletTagParser parses body applet tag from response func bodyAppletTagParser(resp *navigation.Response) (navigationRequests []*navigation.Request) { resp.Reader.Find("applet").Each(func(i int, item *goquery.Selection) { src, ok := item.Attr("archive") if ok && src != "" { navigationRequests = append(navigationRequests, navigation.NewNavigationRequestURLFromResponse(src, resp.Resp.Request.URL.String(), "applet", "archive", resp)) } srcCodebase, ok := item.Attr("codebase") if ok && srcCodebase != "" { navigationRequests = append(navigationRequests, navigation.NewNavigationRequestURLFromResponse(srcCodebase, resp.Resp.Request.URL.String(), "applet", "codebase", resp)) } }) return } // bodyImgTagParser parses Img tag from response func bodyImgTagParser(resp *navigation.Response) (navigationRequests []*navigation.Request) { resp.Reader.Find("img").Each(func(i int, item *goquery.Selection) { srcDynsrc, ok := item.Attr("dynsrc") if ok && srcDynsrc != "" { navigationRequests = append(navigationRequests, navigation.NewNavigationRequestURLFromResponse(srcDynsrc, resp.Resp.Request.URL.String(), "img", "dynsrc", resp)) } srcLongdesc, ok := item.Attr("longdesc") if ok && srcLongdesc != "" { navigationRequests = append(navigationRequests, navigation.NewNavigationRequestURLFromResponse(srcLongdesc, resp.Resp.Request.URL.String(), "img", "longdesc", resp)) } srcLowsrc, ok := item.Attr("lowsrc") if ok && srcLowsrc != "" { navigationRequests = append(navigationRequests, navigation.NewNavigationRequestURLFromResponse(srcLowsrc, resp.Resp.Request.URL.String(), "img", "lowsrc", resp)) } src, ok := item.Attr("src") if ok && src != "" && src != "#" { if strings.HasPrefix(src, "data:") { // TODO: Add data:uri/data:image parsing return } navigationRequests = append(navigationRequests, navigation.NewNavigationRequestURLFromResponse(src, resp.Resp.Request.URL.String(), "img", "src", resp)) } srcSet, ok := item.Attr("srcset") if ok && srcSet != "" { for _, value := range utils.ParseSRCSetTag(srcSet) { navigationRequests = append(navigationRequests, navigation.NewNavigationRequestURLFromResponse(value, resp.Resp.Request.URL.String(), "img", "srcset", resp)) } } }) return } // bodyObjectTagParser parses object tag from response func bodyObjectTagParser(resp *navigation.Response) (navigationRequests []*navigation.Request) { resp.Reader.Find("object").Each(func(i int, item *goquery.Selection) { srcData, ok := item.Attr("data") if ok && srcData != "" { navigationRequests = append(navigationRequests, navigation.NewNavigationRequestURLFromResponse(srcData, resp.Resp.Request.URL.String(), "src", "data", resp)) } srcCodebase, ok := item.Attr("codebase") if ok && srcCodebase != "" { navigationRequests = append(navigationRequests, navigation.NewNavigationRequestURLFromResponse(srcCodebase, resp.Resp.Request.URL.String(), "src", "codebase", resp)) } item.Find("param").Each(func(i int, s *goquery.Selection) { srcValue, ok := s.Attr("value") if ok && srcValue != "" { navigationRequests = append(navigationRequests, navigation.NewNavigationRequestURLFromResponse(srcValue, resp.Resp.Request.URL.String(), "src", "value", resp)) } }) }) return } // bodySvgTagParser parses svg tag from response func bodySvgTagParser(resp *navigation.Response) (navigationRequests []*navigation.Request) { resp.Reader.Find("svg").Each(func(i int, item *goquery.Selection) { item.Find("image").Each(func(i int, s *goquery.Selection) { hrefData, ok := s.Attr("href") if ok && hrefData != "" { navigationRequests = append(navigationRequests, navigation.NewNavigationRequestURLFromResponse(hrefData, resp.Resp.Request.URL.String(), "svg", "image-href", resp)) } }) item.Find("script").Each(func(i int, s *goquery.Selection) { hrefData, ok := s.Attr("href") if ok && hrefData != "" { navigationRequests = append(navigationRequests, navigation.NewNavigationRequestURLFromResponse(hrefData, resp.Resp.Request.URL.String(), "svg", "script-href", resp)) } }) }) return } // bodyTableTagParser parses table tag from response func bodyTableTagParser(resp *navigation.Response) (navigationRequests []*navigation.Request) { resp.Reader.Find("table").Each(func(i int, item *goquery.Selection) { srcData, ok := item.Attr("background") if ok && srcData != "" { navigationRequests = append(navigationRequests, navigation.NewNavigationRequestURLFromResponse(srcData, resp.Resp.Request.URL.String(), "table", "background", resp)) } item.Find("td").Each(func(i int, s *goquery.Selection) { srcValue, ok := s.Attr("background") if ok && srcValue != "" { navigationRequests = append(navigationRequests, navigation.NewNavigationRequestURLFromResponse(srcValue, resp.Resp.Request.URL.String(), "table", "td-background", resp)) } }) }) return } // bodyVideoTagParser parses video tag from response func bodyVideoTagParser(resp *navigation.Response) (navigationRequests []*navigation.Request) { resp.Reader.Find("video").Each(func(i int, item *goquery.Selection) { src, ok := item.Attr("src") if ok && src != "" { navigationRequests = append(navigationRequests, navigation.NewNavigationRequestURLFromResponse(src, resp.Resp.Request.URL.String(), "video", "src", resp)) } srcData, ok := item.Attr("poster") if ok && srcData != "" { navigationRequests = append(navigationRequests, navigation.NewNavigationRequestURLFromResponse(srcData, resp.Resp.Request.URL.String(), "video", "poster", resp)) } item.Find("track").Each(func(i int, s *goquery.Selection) { srcValue, ok := s.Attr("src") if ok && srcValue != "" { navigationRequests = append(navigationRequests, navigation.NewNavigationRequestURLFromResponse(srcValue, resp.Resp.Request.URL.String(), "video", "track-src", resp)) } }) }) return } // bodyBlockquoteCiteTagParser parses blockquote cite tag from response func bodyBlockquoteCiteTagParser(resp *navigation.Response) (navigationRequests []*navigation.Request) { resp.Reader.Find("blockquote[cite]").Each(func(i int, item *goquery.Selection) { src, ok := item.Attr("cite") if ok && src != "" { navigationRequests = append(navigationRequests, navigation.NewNavigationRequestURLFromResponse(src, resp.Resp.Request.URL.String(), "blockquote", "cite", resp)) } }) return } // bodyFrameSrcTagParser parses frame src tag from response func bodyFrameSrcTagParser(resp *navigation.Response) (navigationRequests []*navigation.Request) { resp.Reader.Find("frame[src]").Each(func(i int, item *goquery.Selection) { src, ok := item.Attr("src") if ok && src != "" { navigationRequests = append(navigationRequests, navigation.NewNavigationRequestURLFromResponse(src, resp.Resp.Request.URL.String(), "frame", "src", resp)) } }) return } // bodyMapAreaPingTagParser parses map area ping tag from response func bodyMapAreaPingTagParser(resp *navigation.Response) (navigationRequests []*navigation.Request) { resp.Reader.Find("area[ping]").Each(func(i int, item *goquery.Selection) { src, ok := item.Attr("ping") if ok && src != "" { navigationRequests = append(navigationRequests, navigation.NewNavigationRequestURLFromResponse(src, resp.Resp.Request.URL.String(), "area", "ping", resp)) } }) return } // bodyBaseHrefTagParser parses base href tag from response func bodyBaseHrefTagParser(resp *navigation.Response) (navigationRequests []*navigation.Request) { resp.Reader.Find("base[href]").Each(func(i int, item *goquery.Selection) { src, ok := item.Attr("href") if ok && src != "" { navigationRequests = append(navigationRequests, navigation.NewNavigationRequestURLFromResponse(src, resp.Resp.Request.URL.String(), "base", "href", resp)) } }) return } // bodyImportImplementationTagParser parses import implementation tag from response func bodyImportImplementationTagParser(resp *navigation.Response) (navigationRequests []*navigation.Request) { resp.Reader.Find("import[implementation]").Each(func(i int, item *goquery.Selection) { src, ok := item.Attr("implementation") if ok && src != "" { navigationRequests = append(navigationRequests, navigation.NewNavigationRequestURLFromResponse(src, resp.Resp.Request.URL.String(), "import", "implementation", resp)) } }) return } // bodyButtonFormactionTagParser parses button formaction tag from response func bodyButtonFormactionTagParser(resp *navigation.Response) (navigationRequests []*navigation.Request) { resp.Reader.Find("button[formaction]").Each(func(i int, item *goquery.Selection) { src, ok := item.Attr("formaction") if ok && src != "" { navigationRequests = append(navigationRequests, navigation.NewNavigationRequestURLFromResponse(src, resp.Resp.Request.URL.String(), "button", "formaction", resp)) } }) return } // bodyHtmlManifestTagParser parses body manifest tag from response func bodyHtmlManifestTagParser(resp *navigation.Response) (navigationRequests []*navigation.Request) { resp.Reader.Find("html[manifest]").Each(func(i int, item *goquery.Selection) { src, ok := item.Attr("manifest") if ok && src != "" { navigationRequests = append(navigationRequests, navigation.NewNavigationRequestURLFromResponse(src, resp.Resp.Request.URL.String(), "html", "manifest", resp)) } }) return } // bodyHtmlDoctypeTagParser parses body doctype tag from response func bodyHtmlDoctypeTagParser(resp *navigation.Response) (navigationRequests []*navigation.Request) { if len(resp.Reader.Nodes) < 1 || resp.Reader.Nodes[0].FirstChild == nil { return } docTypeNode := resp.Reader.Nodes[0].FirstChild if docTypeNode.Type != html.DoctypeNode { return } if len(docTypeNode.Attr) == 0 || strings.ToLower(docTypeNode.Attr[0].Key) != "system" { return } navigationRequests = append(navigationRequests, navigation.NewNavigationRequestURLFromResponse(docTypeNode.Attr[0].Val, resp.Resp.Request.URL.String(), "html", "doctype", resp)) return } // bodyFormTagParser parses forms from response func bodyFormTagParser(resp *navigation.Response) (navigationRequests []*navigation.Request) { resp.Reader.Find("form").Each(func(i int, item *goquery.Selection) { href, _ := item.Attr("action") encType, ok := item.Attr("enctype") if !ok || encType == "" { encType = "application/x-www-form-urlencoded" } method, _ := item.Attr("method") if method == "" { method = "GET" } method = strings.ToUpper(method) actionURL := resp.AbsoluteURL(href) if actionURL == "" { return } parsed, err := urlutil.Parse(actionURL) if err != nil { gologger.Warning().Msgf("bodyFormTagParser :failed to parse url %v got %v", actionURL, err) return } isMultipartForm := strings.HasPrefix(encType, "multipart/") queryValuesWriter := urlutil.NewOrderedParams() queryValuesWriter.IncludeEquals = true var sb strings.Builder var multipartWriter *multipart.Writer if isMultipartForm { multipartWriter = multipart.NewWriter(&sb) } // Get the form field suggestions for all elements in the form formFields := []interface{}{} item.Find("input, select, textarea").Each(func(index int, item *goquery.Selection) { if len(item.Nodes) == 0 { return } formFields = append(formFields, utils.ConvertGoquerySelectionToFormField(item)) }) dataMap := utils.FormFillSuggestions(formFields) dataMap.Iterate(func(key, value string) bool { if key == "" { return true } if isMultipartForm { _ = multipartWriter.WriteField(key, value) } else { queryValuesWriter.Set(key, value) } return true }) // Guess content-type var contentType string if multipartWriter != nil { _ = multipartWriter.Close() contentType = multipartWriter.FormDataContentType() } else { contentType = encType } req := &navigation.Request{ Method: method, URL: actionURL, Depth: resp.Depth, RootHostname: resp.RootHostname, Tag: "form", Attribute: "action", Source: resp.Resp.Request.URL.String(), } switch method { case "GET": parsed.Params.Merge(queryValuesWriter.Encode()) req.URL = parsed.String() case "POST": if multipartWriter != nil { req.Body = sb.String() } else { req.Body = queryValuesWriter.Encode() } req.Headers = make(map[string]string) req.Headers["Content-Type"] = contentType } navigationRequests = append(navigationRequests, req) }) return } // bodyMetaContentTagParser parses meta content tag from response func bodyMetaContentTagParser(resp *navigation.Response) (navigationRequests []*navigation.Request) { resp.Reader.Find("meta").Each(func(i int, item *goquery.Selection) { header, ok := item.Attr("content") if !ok { return } extracted := utils.ExtractRelativeEndpoints(header) for _, item := range extracted { navigationRequests = append(navigationRequests, navigation.NewNavigationRequestURLFromResponse(item, resp.Resp.Request.URL.String(), "meta", "refresh", resp)) } }) return } func bodyHtmxAttrParser(resp *navigation.Response) (navigationRequests []*navigation.Request) { // exclude hx-delete resp.Reader.Find("[hx-get],[hx-post],[hx-put],[hx-patch]").Each(func(i int, item *goquery.Selection) { req := &navigation.Request{ RootHostname: resp.RootHostname, Depth: resp.Depth, Source: resp.Resp.Request.URL.String(), Tag: "htmx", } if hxGet, ok := item.Attr("hx-get"); ok && hxGet != "" { req.Method = http.MethodGet req.URL = resp.AbsoluteURL(hxGet) req.Attribute = "hx-get" navigationRequests = append(navigationRequests, req) } if hxPost, ok := item.Attr(("hx-post")); ok && hxPost != "" { req.Method = http.MethodPost req.URL = resp.AbsoluteURL(hxPost) req.Attribute = "hx-post" navigationRequests = append(navigationRequests, req) } if hxPut, ok := item.Attr(("hx-put")); ok && hxPut != "" { req.Method = http.MethodPut req.URL = resp.AbsoluteURL(hxPut) req.Attribute = "hx-put" navigationRequests = append(navigationRequests, req) } if hxPatch, ok := item.Attr(("hx-patch")); ok && hxPatch != "" { req.Method = http.MethodPatch req.URL = resp.AbsoluteURL(hxPatch) req.Attribute = "hx-patch" navigationRequests = append(navigationRequests, req) } }) return } // ------------------------------------------------------------------------- // Begin JS Regex based parsers // ------------------------------------------------------------------------- // scriptContentRegexParser parses script content endpoints from response func scriptContentRegexParser(resp *navigation.Response) (navigationRequests []*navigation.Request) { resp.Reader.Find("script").Each(func(i int, item *goquery.Selection) { text := item.Text() if text == "" { return } endpoints := utils.ExtractRelativeEndpoints(text) for _, item := range endpoints { navigationRequests = append(navigationRequests, navigation.NewNavigationRequestURLFromResponse(item, resp.Resp.Request.URL.String(), "script", "text", resp)) } }) return } // scriptJSFileRegexParser parses relative endpoints from js file pages func scriptJSFileRegexParser(resp *navigation.Response) (navigationRequests []*navigation.Request) { // Only process javascript file based on path or content type // CSS, JS are supported for relative endpoint extraction. contentType := resp.Resp.Header.Get("Content-Type") if !stringsutil.HasSuffixAny(resp.Resp.Request.URL.Path, ".js", ".css") && !strings.Contains(contentType, "/javascript") { return } endpointsItems := utils.ExtractRelativeEndpoints(string(resp.Body)) for _, item := range endpointsItems { navigationRequests = append(navigationRequests, navigation.NewNavigationRequestURLFromResponse(item, resp.Resp.Request.URL.String(), "js", "regex", resp)) } return } // bodyScrapeEndpointsParser parses scraped URLs from HTML body func bodyScrapeEndpointsParser(resp *navigation.Response) (navigationRequests []*navigation.Request) { endpoints := utils.ExtractBodyEndpoints(string(resp.Body)) for _, item := range endpoints { navigationRequests = append(navigationRequests, navigation.NewNavigationRequestURLFromResponse(item, resp.Resp.Request.URL.String(), "html", "regex", resp)) } return } // customFieldRegexParser parses custom regex from HTML body and header func customFieldRegexParser(resp *navigation.Response) (navigationRequests []*navigation.Request) { var customField = make(map[string][]string) for _, v := range output.CustomFieldsMap { results := []string{} for _, re := range v.CompileRegex { matches := [][]string{} // read body if v.Part == output.Body.ToString() || v.Part == output.Response.ToString() { matches = re.FindAllStringSubmatch(string(resp.Body), -1) } // read header if v.Part == output.Header.ToString() || v.Part == output.Response.ToString() { for key, v := range resp.Resp.Header { header := key + ": " + strings.Join(v, "\n") headerMatches := re.FindAllStringSubmatch(header, -1) matches = append(matches, headerMatches...) } } for _, match := range matches { if len(match) < (v.Group + 1) { continue } matchString := match[v.Group] results = append(results, matchString) } } if len(results) > 0 { customField[v.GetName()] = results } } if len(customField) != 0 { navigationRequests = append(navigationRequests, &navigation.Request{ Method: "GET", URL: resp.Resp.Request.URL.String(), Depth: resp.Depth, CustomFields: customField, }) } return } ``` The content has been capped at 50000 tokens, and files over NaN bytes have been omitted. The user could consider applying other filters to refine the result. The better and more specific the context, the better the LLM can follow instructions. If the context seems verbose, the user can refine the filter using uithub. Thank you for using https://uithub.com - Perfect LLM context for any GitHub repo.