├── useragent_data.go ├── .gitignore ├── .github ├── release.yml ├── workflows │ ├── dep-auto-merge.yml │ ├── lint-test.yml │ ├── build-test.yml │ ├── useragent-update.yml │ └── autorelease-tag.yml ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature_request.md │ └── issue-report.md └── dependabot.yml ├── examples └── simple.go ├── useragent_test.go ├── README.md ├── LICENSE ├── go.mod ├── cmd ├── ua │ └── main.go └── update-useragent │ └── main.go ├── useragent.go ├── filter.go └── go.sum /useragent_data.go: -------------------------------------------------------------------------------- 1 | package useragent 2 | 3 | import ( 4 | _ "embed" 5 | "encoding/json" 6 | ) 7 | 8 | //go:embed useragent_data.json 9 | var userAgentsData string 10 | 11 | // initialize user agents data 12 | func init() { 13 | if err := json.Unmarshal([]byte(userAgentsData), &UserAgents); err != nil { 14 | panic(err) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | .vscode 18 | .devcontainer -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | authors: 4 | - dependabot 5 | categories: 6 | - title: 🎉 New Features 7 | labels: 8 | - "Type: Enhancement" 9 | - title: 🐞 Bugs Fixes 10 | labels: 11 | - "Type: Bug" 12 | - title: 🔨 Maintenance 13 | labels: 14 | - "Type: Maintenance" 15 | - title: Other Changes 16 | labels: 17 | - "*" -------------------------------------------------------------------------------- /examples/simple.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/projectdiscovery/useragent" 7 | ) 8 | 9 | func main() { 10 | filters := []useragent.Filter{ 11 | useragent.Chrome, 12 | useragent.Mozilla, 13 | } 14 | 15 | max := 10 16 | uas, err := useragent.PickWithFilters(max, filters...) 17 | if err != nil { 18 | panic(err) 19 | } 20 | 21 | for _, v := range uas { 22 | fmt.Println(v.Raw) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /useragent_test.go: -------------------------------------------------------------------------------- 1 | package useragent 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestPick(t *testing.T) { 10 | uas, err := Pick(2) 11 | require.Nil(t, err, "could not pick user agent") 12 | require.Len(t, uas, 2, "unexpected length") 13 | } 14 | 15 | func TestPickWithFilters(t *testing.T) { 16 | uas, err := PickWithFilters(1, Computer) 17 | require.Nil(t, err, "could not pick user agent") 18 | require.Len(t, uas, 1, "unexpected length") 19 | } 20 | -------------------------------------------------------------------------------- /.github/workflows/dep-auto-merge.yml: -------------------------------------------------------------------------------- 1 | name: 🤖 dep auto merge 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | workflow_dispatch: 8 | 9 | permissions: 10 | pull-requests: write 11 | issues: write 12 | repository-projects: write 13 | 14 | jobs: 15 | automerge: 16 | runs-on: ubuntu-latest 17 | if: github.actor == 'dependabot[bot]' 18 | steps: 19 | - uses: actions/checkout@v3 20 | with: 21 | token: ${{ secrets.DEPENDABOT_PAT }} 22 | 23 | - uses: ahmadnassri/action-dependabot-auto-merge@v2 24 | with: 25 | github-token: ${{ secrets.DEPENDABOT_PAT }} 26 | target: all -------------------------------------------------------------------------------- /.github/workflows/lint-test.yml: -------------------------------------------------------------------------------- 1 | name: 🙏🏻 Lint Test 2 | on: 3 | pull_request: 4 | paths: 5 | - '**.go' 6 | - '**.mod' 7 | workflow_dispatch: 8 | 9 | jobs: 10 | lint: 11 | name: Lint Test 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v4 16 | - name: Set up Go 17 | uses: actions/setup-go@v4 18 | with: 19 | go-version: 1.24.x 20 | - name: Run golangci-lint 21 | uses: golangci/golangci-lint-action@v3.7.0 22 | with: 23 | version: latest 24 | args: --timeout 5m 25 | working-directory: . 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | 3 | contact_links: 4 | - name: Ask an question / advise on using useragent 5 | url: https://github.com/projectdiscovery/useragent/discussions/categories/q-a 6 | about: Ask a question or request support for using useragent 7 | 8 | - name: Share idea / feature to discuss for useragent 9 | url: https://github.com/projectdiscovery/useragent/discussions/categories/ideas 10 | about: Share idea / feature to discuss for useragent 11 | 12 | - name: Connect with PD Team (Discord) 13 | url: https://discord.gg/projectdiscovery 14 | about: Connect with PD Team for direct communication -------------------------------------------------------------------------------- /.github/workflows/build-test.yml: -------------------------------------------------------------------------------- 1 | name: 🔨 Build Test 2 | on: 3 | pull_request: 4 | paths: 5 | - '**.go' 6 | - '**.mod' 7 | workflow_dispatch: 8 | 9 | 10 | jobs: 11 | build: 12 | name: Test Builds 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Set up Go 16 | uses: actions/setup-go@v4 17 | with: 18 | go-version: 1.24.x 19 | 20 | - name: Check out code 21 | uses: actions/checkout@v4 22 | 23 | - name: Build 24 | run: go build ./... 25 | working-directory: . 26 | 27 | - name: Test 28 | run: go test ./... 29 | 30 | - name: Run Example 31 | run: go run . 32 | working-directory: examples/ 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Request feature to implement in this project 4 | labels: 'Type: Enhancement' 5 | --- 6 | 7 | 13 | 14 | ### Please describe your feature request: 15 | 16 | 17 | ### Describe the use case of this feature: 18 | 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # useragent 2 | A comprehensive and categorized collection of User Agents. 3 | 4 | ## Installation Instructions 5 | 6 | To install the useragent tool, use the following command: 7 | 8 | ```sh 9 | go install -v github.com/projectdiscovery/useragent/cmd/ua@latest 10 | ``` 11 | 12 | ## Usage 13 | 14 | To display help for the tool, use the following command: 15 | 16 | ```sh 17 | ua -h 18 | ``` 19 | 20 | This will display all the flags supported by the tool: 21 | 22 | ```sh 23 | ua is a straightforward tool to query and filter user agents 24 | 25 | Usage: 26 | ./ua [flags] 27 | 28 | Flags: 29 | -list list all the categorized tags of user-agent 30 | -l, -limit int number of user-agent to list (use -1 to list all) (default 10) 31 | -t, -tag string[] list user-agent for given tag 32 | ``` 33 | 34 | The useragent tool is designed to be simple and efficient, making it easy to query and filter user agents based on your specific needs. 35 | 36 | ## Credits 37 | 38 | This tool utilizes user agent data obtained from [WhatIsMyBrowser.com](https://www.whatismybrowser.com). 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 ProjectDiscovery 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/useragent-update.yml: -------------------------------------------------------------------------------- 1 | name: 💡User-Agent Update 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: '0 0 * * 0' 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout Repo 13 | uses: actions/checkout@v4 14 | with: 15 | fetch-depth: 0 16 | 17 | - name: Setup golang 18 | uses: actions/setup-go@v4 19 | with: 20 | go-version: 1.24.x 21 | 22 | - name: Update User-Agents Data 23 | env: 24 | WHATISMYBROWSER_KEY: ${{ secrets.WHATISMYBROWSER_KEY }} 25 | run: | 26 | go run . 27 | working-directory: cmd/update-useragent 28 | 29 | - name: Create Pull Request 30 | uses: peter-evans/create-pull-request@v6 31 | with: 32 | token: ${{ secrets.GITHUB_TOKEN }} 33 | commit-message: "Weekly useragent update" 34 | title: "Weekly useragent update" 35 | body: "This PR updates the useragent data." 36 | branch: 'useragent-update' 37 | delete-branch: true 38 | base: 'main' 39 | -------------------------------------------------------------------------------- /.github/workflows/autorelease-tag.yml: -------------------------------------------------------------------------------- 1 | name: 🔖 Tag Release 2 | 3 | on: 4 | workflow_run: 5 | workflows: ["💡User-Agent Update"] 6 | types: 7 | - completed 8 | workflow_dispatch: 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Check out code 15 | uses: actions/checkout@v4 16 | with: 17 | fetch-depth: 0 18 | 19 | - name: Get Commit Count 20 | id: get_commit 21 | run: git rev-list `git rev-list --tags --no-walk --max-count=1`..HEAD --count | xargs -I {} echo COMMIT_COUNT={} >> $GITHUB_OUTPUT 22 | 23 | - name: Create release and tag 24 | if: ${{ steps.get_commit.outputs.COMMIT_COUNT > 0 }} 25 | id: tag_version 26 | uses: mathieudutour/github-tag-action@v6.1 27 | with: 28 | github_token: ${{ secrets.GITHUB_TOKEN }} 29 | 30 | - name: Create a GitHub release 31 | if: ${{ steps.get_commit.outputs.COMMIT_COUNT > 0 }} 32 | uses: actions/create-release@v1 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 35 | with: 36 | tag_name: ${{ steps.tag_version.outputs.new_tag }} 37 | release_name: Release ${{ steps.tag_version.outputs.new_tag }} 38 | body: ${{ steps.tag_version.outputs.changelog }} -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | 9 | # Maintain dependencies for go modules 10 | - package-ecosystem: "gomod" 11 | directory: "/" 12 | schedule: 13 | interval: "weekly" 14 | target-branch: "main" 15 | commit-message: 16 | prefix: "chore" 17 | include: "scope" 18 | labels: 19 | - "Type: Maintenance" 20 | allow: 21 | - dependency-name: "github.com/projectdiscovery/*" 22 | 23 | # # Maintain dependencies for docker 24 | # - package-ecosystem: "docker" 25 | # directory: "/" 26 | # schedule: 27 | # interval: "weekly" 28 | # target-branch: "dev" 29 | # commit-message: 30 | # prefix: "chore" 31 | # include: "scope" 32 | # labels: 33 | # - "Type: Maintenance" 34 | # 35 | # # Maintain dependencies for GitHub Actions 36 | # - package-ecosystem: "github-actions" 37 | # directory: "/" 38 | # schedule: 39 | # interval: "weekly" 40 | # target-branch: "dev" 41 | # commit-message: 42 | # prefix: "chore" 43 | # include: "scope" 44 | # labels: 45 | # - "Type: Maintenance" -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/issue-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Issue report 3 | about: Create a report to help us to improve the project 4 | labels: 'Type: Bug' 5 | 6 | --- 7 | 8 | 13 | 14 | 15 | 16 | ### useragent version: 17 | 18 | 19 | 20 | 21 | ### Current Behavior: 22 | 23 | 24 | ### Expected Behavior: 25 | 26 | 27 | ### Steps To Reproduce: 28 | 33 | 34 | 35 | ### Anything else: 36 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/projectdiscovery/useragent 2 | 3 | go 1.24.0 4 | 5 | toolchain go1.24.2 6 | 7 | require ( 8 | github.com/projectdiscovery/goflags v0.1.74 9 | github.com/projectdiscovery/utils v0.8.0 10 | github.com/stretchr/testify v1.11.1 11 | ) 12 | 13 | require ( 14 | github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect 15 | github.com/aymerick/douceur v0.2.0 // indirect 16 | github.com/cnf/structhash v0.0.0-20250313080605-df4c6cc74a9a // indirect 17 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 18 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect 19 | github.com/gorilla/css v1.0.1 // indirect 20 | github.com/mattn/go-isatty v0.0.20 // indirect 21 | github.com/microcosm-cc/bluemonday v1.0.27 // indirect 22 | github.com/miekg/dns v1.1.68 // indirect 23 | github.com/pkg/errors v0.9.1 // indirect 24 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 25 | github.com/projectdiscovery/blackrock v0.0.1 // indirect 26 | github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d // indirect 27 | github.com/tidwall/gjson v1.18.0 // indirect 28 | github.com/tidwall/match v1.2.0 // indirect 29 | github.com/tidwall/pretty v1.2.1 // indirect 30 | golang.org/x/exp v0.0.0-20250911091902-df9299821621 // indirect 31 | golang.org/x/mod v0.29.0 // indirect 32 | golang.org/x/net v0.47.0 // indirect 33 | golang.org/x/sync v0.18.0 // indirect 34 | golang.org/x/sys v0.38.0 // indirect 35 | golang.org/x/tools v0.38.0 // indirect 36 | gopkg.in/yaml.v3 v3.0.1 // indirect 37 | ) 38 | -------------------------------------------------------------------------------- /cmd/ua/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/projectdiscovery/goflags" 8 | "github.com/projectdiscovery/useragent" 9 | ) 10 | 11 | type Options struct { 12 | List bool 13 | Max int 14 | Tags goflags.StringSlice 15 | } 16 | 17 | func main() { 18 | opts := parseInput() 19 | 20 | if opts.List { 21 | for tag := range useragent.FilterMap { 22 | fmt.Println(tag) 23 | } 24 | } else if len(opts.Tags) != 0 { 25 | signatures := []useragent.Filter{} 26 | for _, v := range opts.Tags { 27 | if v == "all" { 28 | for _, filter := range useragent.FilterMap { 29 | signatures = append(signatures, filter) 30 | } 31 | break 32 | } 33 | 34 | if sig, ok := useragent.FilterMap[v]; ok { 35 | signatures = append(signatures, sig) 36 | } else { 37 | log.Fatalf("tag `%v` not found", v) 38 | } 39 | } 40 | 41 | uas, err := useragent.PickWithFilters(opts.Max, signatures...) 42 | if err != nil { 43 | panic(err) 44 | } 45 | 46 | for _, v := range uas { 47 | fmt.Println(v.Raw) 48 | } 49 | } 50 | 51 | } 52 | 53 | func parseInput() *Options { 54 | opts := Options{} 55 | 56 | flagset := goflags.NewFlagSet() 57 | 58 | flagset.SetDescription("ua is simple tool to query and filter user agents") 59 | 60 | flagset.BoolVar(&opts.List, "list", false, "list all the categorized tags of user-agent") 61 | flagset.IntVarP(&opts.Max, "limit", "l", 10, "number of user-agent to list (use -1 to list all)") 62 | flagset.StringSliceVarP(&opts.Tags, "tag", "t", []string{}, "list user-agent for given tag", goflags.CommaSeparatedStringSliceOptions) 63 | 64 | if err := flagset.Parse(); err != nil { 65 | panic(err) 66 | } 67 | 68 | return &opts 69 | 70 | } 71 | -------------------------------------------------------------------------------- /useragent.go: -------------------------------------------------------------------------------- 1 | package useragent 2 | 3 | import ( 4 | "fmt" 5 | 6 | sliceutil "github.com/projectdiscovery/utils/slice" 7 | ) 8 | 9 | // UserAgents of the package 10 | var UserAgents []*UserAgent 11 | 12 | // UserAgent with tags 13 | type UserAgent struct { 14 | Tags []string 15 | Raw string 16 | } 17 | 18 | // String returns the user agent raw value 19 | func (userAgent *UserAgent) String() string { 20 | return userAgent.Raw 21 | } 22 | 23 | // Pick n items randomly from the available ones 24 | func Pick(n int) ([]*UserAgent, error) { 25 | return PickWithFilters(n) 26 | } 27 | 28 | // Pick n items randomly for the available ones with optional filtering 29 | func PickWithFilters(n int, filters ...Filter) ([]*UserAgent, error) { 30 | if n > len(UserAgents) { 31 | return nil, fmt.Errorf("the database does not contain %d items", n) 32 | } 33 | // filters out wanted ones 34 | var filteredUserAgents []*UserAgent 35 | if len(filters) > 0 { 36 | for _, ua := range UserAgents { 37 | for _, filter := range filters { 38 | if !filter(ua) { 39 | continue 40 | } 41 | filteredUserAgents = append(filteredUserAgents, ua) 42 | } 43 | } 44 | } else { 45 | filteredUserAgents = UserAgents 46 | } 47 | 48 | if n > len(filteredUserAgents) { 49 | return nil, fmt.Errorf("the filtered database does not contain %d items", n) 50 | } 51 | 52 | var userAgents []*UserAgent 53 | 54 | // retrieve all user agents 55 | if n == -1 { 56 | n = len(filteredUserAgents) 57 | } 58 | 59 | for i := 0; i < n; i++ { 60 | userAgent := sliceutil.PickRandom(filteredUserAgents) 61 | userAgents = append(userAgents, userAgent) 62 | } 63 | return userAgents, nil 64 | } 65 | 66 | func PickRandom() *UserAgent { 67 | return sliceutil.PickRandom(UserAgents) 68 | } 69 | -------------------------------------------------------------------------------- /filter.go: -------------------------------------------------------------------------------- 1 | package useragent 2 | 3 | import ( 4 | "strings" 5 | 6 | stringsutil "github.com/projectdiscovery/utils/strings" 7 | ) 8 | 9 | // Filter represent the function signature for a filter 10 | type Filter func(*UserAgent) bool 11 | 12 | // FilterMap contains filter and its respective function signature 13 | var FilterMap map[string]Filter 14 | 15 | // ContainsTagsAny returns true if the user agent contains any of the provided tags 16 | func ContainsTagsAny(userAgent *UserAgent, tags ...string) bool { 17 | for _, tag := range userAgent.Tags { 18 | if stringsutil.ContainsAny(tag, tags...) { 19 | return true 20 | } 21 | } 22 | return false 23 | } 24 | 25 | // ContainsTags returns true if the user agent contains all provided tags 26 | func ContainsTags(userAgent *UserAgent, tags ...string) bool { 27 | foundTags := make(map[string]struct{}) 28 | for _, tag := range userAgent.Tags { 29 | for _, wantedTag := range tags { 30 | if strings.Contains(tag, wantedTag) { 31 | foundTags[tag] = struct{}{} 32 | } 33 | } 34 | } 35 | return len(foundTags) == len(tags) 36 | } 37 | 38 | // Mobile checks if the user agent has typical mobile tags 39 | func Mobile(userAgent *UserAgent) bool { 40 | return ContainsTags(userAgent, "mobile") 41 | } 42 | 43 | // Legacy checks if the user agent falls under legacy category 44 | func Legacy(userAgent *UserAgent) bool { 45 | return ContainsTags(userAgent, "Legacy") 46 | } 47 | 48 | // Google Checks if the user agent has typical GoogleBot tags 49 | func GoogleBot(userAgent *UserAgent) bool { 50 | return ContainsTags(userAgent, "Google", "Spiders") 51 | } 52 | 53 | // Chrome checks if the user agent has typical chrome tags 54 | func Chrome(userAgent *UserAgent) bool { 55 | return ContainsTagsAny(userAgent, "Chrome", "Chromium") 56 | } 57 | 58 | // Mozilla checks if the user agent has typical mozilla firefox tags 59 | func Mozilla(userAgent *UserAgent) bool { 60 | return ContainsTagsAny(userAgent, "Mozilla", "Firefox") 61 | } 62 | 63 | // Safari checks if the user agent has typical safari tags 64 | func Safari(userAgent *UserAgent) bool { 65 | return ContainsTags(userAgent, "Safari") 66 | } 67 | 68 | // Computer checks if the user agent has typical computer tags 69 | func Computer(userAgent *UserAgent) bool { 70 | return ContainsTags(userAgent, "computer") 71 | } 72 | 73 | // Apple checks if the user agent has typical apple tags 74 | func Apple(userAgent *UserAgent) bool { 75 | return ContainsTags(userAgent, "Apple Computer, Inc.") 76 | } 77 | 78 | // Windows checks if the user agent has typical windows tags 79 | func Windows(userAgent *UserAgent) bool { 80 | return ContainsTagsAny(userAgent, "Win32", "Windows") 81 | } 82 | 83 | // Bot checks if the user agent has typical bot tags 84 | func Bot(userAgent *UserAgent) bool { 85 | return ContainsTagsAny(userAgent, "Spiders - Search") 86 | } 87 | 88 | func init() { 89 | FilterMap = map[string]Filter{} 90 | 91 | FilterMap["computer"] = Computer 92 | FilterMap["mobile"] = Mobile 93 | FilterMap["legacy"] = Legacy 94 | FilterMap["chrome"] = Chrome 95 | FilterMap["mozilla"] = Mozilla 96 | FilterMap["googlebot"] = GoogleBot 97 | FilterMap["safari"] = Safari 98 | FilterMap["apple"] = Apple 99 | FilterMap["windows"] = Windows 100 | FilterMap["bot"] = Bot 101 | } 102 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= 2 | github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= 3 | github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= 4 | github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= 5 | github.com/cnf/structhash v0.0.0-20250313080605-df4c6cc74a9a h1:Ohw57yVY2dBTt+gsC6aZdteyxwlxfbtgkFEMTEkwgSw= 6 | github.com/cnf/structhash v0.0.0-20250313080605-df4c6cc74a9a/go.mod h1:pCxVEbcm3AMg7ejXyorUXi6HQCzOIBf7zEDVPtw0/U4= 7 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 8 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 10 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 11 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= 12 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= 13 | github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= 14 | github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= 15 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 16 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 17 | github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= 18 | github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= 19 | github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA= 20 | github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps= 21 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 22 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 23 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 24 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 25 | github.com/projectdiscovery/blackrock v0.0.1 h1:lHQqhaaEFjgf5WkuItbpeCZv2DUIE45k0VbGJyft6LQ= 26 | github.com/projectdiscovery/blackrock v0.0.1/go.mod h1:ANUtjDfaVrqB453bzToU+YB4cUbvBRpLvEwoWIwlTss= 27 | github.com/projectdiscovery/goflags v0.1.74 h1:n85uTRj5qMosm0PFBfsvOL24I7TdWRcWq/1GynhXS7c= 28 | github.com/projectdiscovery/goflags v0.1.74/go.mod h1:UMc9/7dFz2oln+10tv6cy+7WZKTHf9UGhaNkF95emh4= 29 | github.com/projectdiscovery/utils v0.8.0 h1:8d79OCs5xGDNXdKxMUKMY/lgQSUWJMYB1B2Sx+oiqkQ= 30 | github.com/projectdiscovery/utils v0.8.0/go.mod h1:CU6tjtyTRxBrnNek+GPJplw4IIHcXNZNKO09kWgqTdg= 31 | github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d h1:hrujxIzL1woJ7AwssoOcM/tq5JjjG2yYOc8odClEiXA= 32 | github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU= 33 | github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 34 | github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 35 | github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= 36 | github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= 37 | github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= 38 | github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM= 39 | github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= 40 | github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= 41 | github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= 42 | github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= 43 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 44 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 45 | golang.org/x/exp v0.0.0-20250911091902-df9299821621 h1:2id6c1/gto0kaHYyrixvknJ8tUK/Qs5IsmBtrc+FtgU= 46 | golang.org/x/exp v0.0.0-20250911091902-df9299821621/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk= 47 | golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= 48 | golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= 49 | golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= 50 | golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= 51 | golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= 52 | golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 53 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 54 | golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= 55 | golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 56 | golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= 57 | golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= 58 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 59 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 60 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 61 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 62 | -------------------------------------------------------------------------------- /cmd/update-useragent/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/tls" 5 | "encoding/json" 6 | "flag" 7 | "fmt" 8 | "io" 9 | "log" 10 | "net/http" 11 | "os" 12 | "regexp" 13 | 14 | "github.com/projectdiscovery/useragent" 15 | sliceutil "github.com/projectdiscovery/utils/slice" 16 | stringsutil "github.com/projectdiscovery/utils/strings" 17 | ) 18 | 19 | var ( 20 | userAgentData = flag.String("user-agents", "../../useragent_data.json", "File to write user agents to") 21 | browsers = []string{"chrome", "edge", "safari", "firefox", "firefox-esr"} 22 | ) 23 | 24 | func main() { 25 | flag.Parse() 26 | 27 | userAgents := getUserAgents() 28 | if len(userAgents) == 0 { 29 | log.Fatal("Couldn't get user agents. The user agent data will remain unchanged.") 30 | } 31 | data, err := json.Marshal(userAgents) 32 | if err != nil { 33 | log.Fatal(err) 34 | } 35 | 36 | err = os.WriteFile(*userAgentData, data, 0644) 37 | if err != nil { 38 | log.Fatal(err) 39 | } 40 | 41 | } 42 | 43 | func getUserAgents() []*useragent.UserAgent { 44 | var userAgents []*useragent.UserAgent 45 | 46 | httpClient := buildHttpClient() 47 | for _, browser := range browsers { 48 | whatismybrowserApiUrl := fmt.Sprintf(`https://api.whatismybrowser.com/api/v2/user_agent_database_search?order_by=times_seen%%20desc&hardware_type=computer&limit=300&offset=500&software_name=%s`, browser) 49 | req, err := http.NewRequest("GET", whatismybrowserApiUrl, nil) 50 | if err != nil { 51 | continue 52 | } 53 | apiKey := os.Getenv("WHATISMYBROWSER_KEY") 54 | if apiKey == "" { 55 | log.Fatal("API key is empty. Please set the WHATISMYBROWSER_KEY environment variable.") 56 | } 57 | req.Header.Add("X-API-KEY", apiKey) 58 | 59 | resp, err := httpClient.Do(req) 60 | if err != nil { 61 | continue 62 | } 63 | 64 | data, err := io.ReadAll(resp.Body) 65 | if err != nil { 66 | continue 67 | } 68 | 69 | var whatismybrowserResponse WhatismybrowserResponse 70 | err = json.Unmarshal(data, &whatismybrowserResponse) 71 | if err != nil { 72 | continue 73 | } 74 | for _, userAgent := range whatismybrowserResponse.SearchResults.UserAgents { 75 | if stringsutil.ContainsAnyI(userAgent.UserAgent, 76 | "sleep", "timeout", "get-help", "start", "system", 77 | "Functionize", "Edg/", "assetnote", "gzip", "norton", 78 | "avast", "ccleaner", "avg", "xtpt", "promptmanager", "SznProhlizec", 79 | "(0-0)", "unknown", "ddg", "opx", "RDDocuments", "Reeder", 80 | "Topee", "TulipSAT", "Opendium", "DingTalk", "Gear", 81 | "GoodAccess", "uacq") || 82 | hasUidSuffix(userAgent.UserAgent) { 83 | continue 84 | } 85 | 86 | tags := buildTags(userAgent) 87 | userAgent := &useragent.UserAgent{ 88 | Raw: userAgent.UserAgent, 89 | Tags: sliceutil.Dedupe(sliceutil.PruneEmptyStrings(tags)), 90 | } 91 | userAgents = append(userAgents, userAgent) 92 | } 93 | } 94 | return userAgents 95 | } 96 | 97 | func hasUidSuffix(s string) bool { 98 | var guidPattern = regexp.MustCompile(`\(?[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}\)?$`) 99 | return guidPattern.MatchString(s) 100 | } 101 | 102 | func buildTags(userAgent UserAgents) []string { 103 | var tags []string 104 | tags = append(tags, userAgent.Parse.SimpleSoftwareString) 105 | tags = append(tags, fmt.Sprintf("%v", userAgent.Parse.SimpleSubDescriptionString)) 106 | tags = append(tags, fmt.Sprintf("%v", userAgent.Parse.SimpleOperatingPlatformString)) 107 | tags = append(tags, userAgent.Parse.Software) 108 | tags = append(tags, userAgent.Parse.SoftwareName) 109 | tags = append(tags, userAgent.Parse.SoftwareNameCode) 110 | tags = append(tags, userAgent.Parse.SoftwareVersion) 111 | tags = append(tags, userAgent.Parse.SoftwareVersionFull) 112 | tags = append(tags, userAgent.Parse.OperatingSystem) 113 | tags = append(tags, userAgent.Parse.OperatingSystemName) 114 | tags = append(tags, userAgent.Parse.OperatingSystemNameCode) 115 | tags = append(tags, userAgent.Parse.OperatingSystemVersion) 116 | tags = append(tags, userAgent.Parse.OperatingSystemVersionFull) 117 | tags = append(tags, fmt.Sprintf("%v", userAgent.Parse.OperatingPlatform)) 118 | tags = append(tags, fmt.Sprintf("%v", userAgent.Parse.OperatingPlatformCode)) 119 | tags = append(tags, fmt.Sprintf("%v", userAgent.Parse.OperatingPlatformVendorName)) 120 | tags = append(tags, userAgent.Parse.SoftwareType) 121 | tags = append(tags, userAgent.Parse.SoftwareSubType) 122 | tags = append(tags, userAgent.Parse.HardwareType) 123 | tags = append(tags, fmt.Sprintf("%v", userAgent.Parse.HardwareSubType)) 124 | tags = append(tags, fmt.Sprintf("%v", userAgent.Parse.HardwareSubSubType)) 125 | tags = append(tags, userAgent.Parse.SoftwareTypeSpecific) 126 | tags = append(tags, userAgent.Parse.HardwareTypeSpecific) 127 | tags = append(tags, userAgent.Parse.LayoutEngineName) 128 | 129 | return tags 130 | } 131 | 132 | // buildHttpClient with common options 133 | func buildHttpClient() *http.Client { 134 | transport := http.Transport{ 135 | Proxy: http.ProxyFromEnvironment, 136 | TLSClientConfig: &tls.Config{ 137 | InsecureSkipVerify: true, 138 | }, 139 | } 140 | 141 | return &http.Client{Transport: &transport} 142 | } 143 | 144 | type UserAgentMetaData struct { 145 | ID int `json:"id"` 146 | TimesSeen int `json:"times_seen"` 147 | LastSeenAt string `json:"last_seen_at"` 148 | } 149 | 150 | type Parse struct { 151 | SimpleSoftwareString string `json:"simple_software_string"` 152 | SimpleSubDescriptionString any `json:"simple_sub_description_string"` 153 | SimpleOperatingPlatformString any `json:"simple_operating_platform_string"` 154 | Software string `json:"software"` 155 | SoftwareName string `json:"software_name"` 156 | SoftwareNameCode string `json:"software_name_code"` 157 | SoftwareVersion string `json:"software_version"` 158 | SoftwareVersionFull string `json:"software_version_full"` 159 | OperatingSystem string `json:"operating_system"` 160 | OperatingSystemName string `json:"operating_system_name"` 161 | OperatingSystemNameCode string `json:"operating_system_name_code"` 162 | OperatingSystemVersion string `json:"operating_system_version"` 163 | OperatingSystemVersionFull string `json:"operating_system_version_full"` 164 | OperatingPlatform any `json:"operating_platform"` 165 | OperatingPlatformCode any `json:"operating_platform_code"` 166 | OperatingPlatformVendorName any `json:"operating_platform_vendor_name"` 167 | SoftwareType string `json:"software_type"` 168 | SoftwareSubType string `json:"software_sub_type"` 169 | HardwareType string `json:"hardware_type"` 170 | HardwareSubType any `json:"hardware_sub_type"` 171 | HardwareSubSubType any `json:"hardware_sub_sub_type"` 172 | SoftwareTypeSpecific string `json:"software_type_specific"` 173 | HardwareTypeSpecific string `json:"hardware_type_specific"` 174 | LayoutEngineName string `json:"layout_engine_name"` 175 | } 176 | 177 | type UserAgents struct { 178 | UserAgent string `json:"user_agent"` 179 | UserAgentMetaData UserAgentMetaData `json:"user_agent_meta_data"` 180 | Parse Parse `json:"parse"` 181 | } 182 | 183 | type SearchParameters struct { 184 | SoftwareName string `json:"software_name"` 185 | HardwareType string `json:"hardware_type"` 186 | } 187 | 188 | type SearchMetaData struct { 189 | NumOfResultsReturned int `json:"num_of_results_returned"` 190 | SearchTookMilliseconds int `json:"search_took_milliseconds"` 191 | SearchParameters SearchParameters `json:"search_parameters"` 192 | } 193 | 194 | type Result struct { 195 | Code string `json:"code"` 196 | MessageCode string `json:"message_code"` 197 | Message string `json:"message"` 198 | } 199 | 200 | type SearchResults struct { 201 | UserAgents []UserAgents `json:"user_agents"` 202 | SearchMetaData SearchMetaData `json:"search_meta_data"` 203 | } 204 | 205 | type WhatismybrowserResponse struct { 206 | SearchResults SearchResults `json:"search_results"` 207 | Result Result `json:"result"` 208 | } 209 | --------------------------------------------------------------------------------