├── .codeclimate.yml ├── .dockerignore ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ └── bug_report.md ├── dependabot.yml ├── stale.yml └── workflows │ ├── build.yml │ ├── client.yml │ ├── codeql-analysis.yml │ ├── dockerimage-next.yml │ ├── homebrew.yml │ └── release.yml ├── .gitignore ├── .goreleaser.yml ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── bin └── .gitkeep ├── build ├── build.go └── build_test.go ├── cmd ├── root.go ├── scan.go ├── scanners.go ├── serve.go └── version.go ├── docs ├── contribute.md ├── getting-started │ ├── go-module-usage.md │ ├── install.md │ ├── scanners.md │ └── usage.md ├── images │ ├── 0e2SMdL.png │ ├── KfrvacR.png │ ├── banner.png │ ├── logo.svg │ ├── logo_white.svg │ └── screenshot.png ├── index.md ├── jetbrains.svg └── resources │ ├── additional-resources.md │ └── formatting.md ├── examples └── plugin │ ├── README.md │ ├── customscanner.go │ └── customscanner_test.go ├── go.mod ├── go.sum ├── lib ├── filter │ ├── filter.go │ └── filter_test.go ├── number │ ├── number.go │ ├── number_test.go │ ├── utils.go │ └── utils_test.go ├── output │ ├── console.go │ ├── console_test.go │ ├── output.go │ └── testdata │ │ ├── console_empty.txt │ │ ├── console_valid.txt │ │ ├── console_valid_recursive.txt │ │ └── console_valid_with_errors.txt └── remote │ ├── googlecse_scanner.go │ ├── googlecse_scanner_test.go │ ├── googlesearch_scanner.go │ ├── googlesearch_scanner_test.go │ ├── init.go │ ├── local_scanner.go │ ├── local_scanner_test.go │ ├── numverify_scanner.go │ ├── numverify_scanner_test.go │ ├── ovh_scanner.go │ ├── ovh_scanner_test.go │ ├── remote.go │ ├── remote_test.go │ ├── scanner.go │ ├── scanner_test.go │ ├── suppliers │ ├── numverify.go │ ├── numverify_test.go │ ├── ovh.go │ └── ovh_test.go │ └── testdata │ ├── .gitignore │ └── invalid.so ├── logs ├── config.go └── init.go ├── main.go ├── mkdocs.yml ├── mocks ├── NumverifySupplier.go ├── NumverifySupplierRequest.go ├── OVHSupplierInterface.go ├── Plugin.go └── Scanner.go ├── support ├── docker │ ├── docker-compose.traefik.yml │ └── docker-compose.yml └── scripts │ └── install ├── test ├── goldenfile │ └── goldenfile.go └── number.go └── web ├── client.go ├── client ├── .gitignore ├── .yarn │ └── releases │ │ └── yarn-3.2.4.cjs ├── .yarnrc.yml ├── README.md ├── cypress.json ├── jest.config.js ├── package.json ├── public │ ├── css │ │ ├── bootstrap-vue.min.css │ │ └── bootstrap.min.css │ ├── icon.svg │ └── index.html ├── src │ ├── App.vue │ ├── assets │ │ └── logo.svg │ ├── components │ │ ├── GoogleSearch.vue │ │ ├── LocalScan.vue │ │ ├── NumverifyScan.vue │ │ ├── OVHScan.vue │ │ └── Scanner.vue │ ├── config │ │ └── index.ts │ ├── main.ts │ ├── router │ │ └── index.ts │ ├── shims-tsx.d.ts │ ├── shims-vue.d.ts │ ├── store │ │ └── index.ts │ ├── utils │ │ └── index.ts │ └── views │ │ ├── NotFound.vue │ │ ├── Number.vue │ │ └── Scan.vue ├── tests │ ├── e2e │ │ ├── .eslintrc.js │ │ ├── plugins │ │ │ └── index.js │ │ ├── specs │ │ │ └── test.js │ │ └── support │ │ │ ├── commands.js │ │ │ └── index.js │ └── unit │ │ ├── config.spec.ts │ │ └── utils.spec.ts ├── tsconfig.json ├── vue.config.js └── yarn.lock ├── controllers.go ├── docs ├── docs.go ├── swagger.json └── swagger.yaml ├── errors.go ├── errors └── errors.go ├── response.go ├── response_test.go ├── server.go ├── server_test.go ├── v2 └── api │ ├── handlers │ ├── init.go │ ├── init_test.go │ ├── numbers.go │ ├── numbers_test.go │ ├── scanners.go │ └── scanners_test.go │ ├── response.go │ ├── response_test.go │ └── server │ └── server.go └── validators.go /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | plugins: 2 | golint: 3 | enabled: true 4 | gofmt: 5 | enabled: true -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | Dockerfile 3 | client/node_modules 4 | *.md 5 | *.out 6 | *.xml 7 | support 8 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [sundowndev] 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | What command did you run ? (hide any personal information such as phone number or ip address) 15 | 16 | **Expected behavior** 17 | A clear and concise description of what you expected to happen. 18 | 19 | **Screenshots (optional)** 20 | If applicable, add screenshots to help explain your problem. 21 | 22 | **Desktop (please complete the following information):** 23 | - OS: [e.g. Windows 10, Ubuntu 18.04 ...] 24 | - PhoneInfoga exact version (run `phoneinfoga version`) 25 | - Go exact version (if running it with Go) (run `go version`) 26 | 27 | **Additional context** 28 | Add any other context about the problem here. 29 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Fetch and update latest `github-actions` pkgs 4 | - package-ecosystem: github-actions 5 | directory: '/' 6 | schedule: 7 | interval: "weekly" 8 | time: '00:00' 9 | open-pull-requests-limit: 2 10 | reviewers: 11 | - sundowndev 12 | assignees: 13 | - sundowndev 14 | commit-message: 15 | prefix: fix 16 | prefix-development: chore 17 | include: scope 18 | # Enable version updates for Docker 19 | - package-ecosystem: "docker" 20 | # Look for a `Dockerfile` in the `root` directory 21 | directory: "/" 22 | # Check for updates once a week 23 | schedule: 24 | interval: "weekly" 25 | open-pull-requests-limit: 2 26 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 60 3 | 4 | # Number of days of inactivity before a stale issue is closed 5 | daysUntilClose: 15 6 | 7 | # Issues with these labels will never be considered stale 8 | exemptLabels: 9 | - pinned 10 | - security 11 | 12 | # Label to use when marking an issue as stale 13 | staleLabel: wontfix 14 | 15 | # Set to true to ignore issues in a project (defaults to false) 16 | exemptProjects: false 17 | 18 | # Set to true to ignore issues in a milestone (defaults to false) 19 | exemptMilestones: false 20 | 21 | # Set to true to ignore issues with an assignee (defaults to false) 22 | exemptAssignees: false 23 | 24 | # Limit the number of actions per hour, from 1-30. Default is 30 25 | limitPerRun: 30 26 | 27 | pulls: 28 | markComment: |- 29 | This pull request has been marked 'stale' due to lack of recent activity. If there is no further activity, the PR will be closed in another 15 days. Thank you for your contribution! 30 | unmarkComment: >- 31 | This pull request is no longer marked for closure. 32 | closeComment: >- 33 | This pull request has been closed due to inactivity. If you feel this is in error, please reopen the pull request or file a new PR with the relevant details. 34 | issues: 35 | markComment: |- 36 | This issue has been marked 'stale' due to lack of recent activity. If there is no further activity, the issue will be closed in another 15 days. Thank you for your contribution! 37 | unmarkComment: >- 38 | This issue is no longer marked for closure. 39 | closeComment: >- 40 | This issue has been closed due to inactivity. If you feel this is in error, please reopen the issue or file a new issue with the relevant details. 41 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | build: 13 | name: Build 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Set up Go 17 | uses: actions/setup-go@v4.1.0 18 | with: 19 | go-version: 1.20.6 20 | id: go 21 | - name: Check out code into the Go module directory 22 | uses: actions/checkout@v4.1.0 23 | 24 | - name: Get dependencies 25 | run: | 26 | go get -v -t -d ./... 27 | 28 | - name: Use Node.js ${{ matrix.node-version }} 29 | uses: actions/setup-node@v3.8.1 30 | with: 31 | node-version: 20.3.1 32 | 33 | - name: Building static assets 34 | run: (cd web/client && yarn install --immutable && yarn build) 35 | 36 | - name: Enforce Go formatted code 37 | run: | 38 | make fmt 39 | if [[ -z $(git status --porcelain) ]]; then 40 | echo "Git directory is clean." 41 | else 42 | echo "Git directory is dirty. Run make fmt locally and commit any formatting fixes or generated code." 43 | git status --porcelain 44 | exit 1 45 | fi 46 | 47 | - name: Install tools 48 | run: make install-tools 49 | 50 | - name: Build 51 | run: make build 52 | 53 | # Temporary disabled lint job because of this issue 54 | # https://github.com/golangci/golangci-lint/issues/3107 55 | # - name: Lint 56 | # run: make lint 57 | 58 | - name: Test 59 | run: go test -race -coverprofile=./c.out -covermode=atomic -v ./... 60 | 61 | - name: Report code coverage 62 | env: 63 | COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }} 64 | run: | 65 | go install github.com/mattn/goveralls@latest 66 | goveralls -coverprofile=./c.out -service=github 67 | -------------------------------------------------------------------------------- /.github/workflows/client.yml: -------------------------------------------------------------------------------- 1 | name: Web client CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | node-version: [20.3.1] 17 | steps: 18 | - uses: actions/checkout@v4.1.0 19 | - name: Use Node.js ${{ matrix.node-version }} 20 | uses: actions/setup-node@v3.8.1 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | - name: Install & lint 24 | run: | 25 | cd web/client 26 | yarn install --immutable 27 | yarn lint 28 | - name: Build 29 | run: | 30 | cd web/client 31 | yarn build 32 | - name: Test 33 | run: | 34 | cd web/client 35 | yarn test:unit 36 | # - name: Upload coverage to Codecov 37 | # uses: codecov/codecov-action@v1.0.2 38 | # with: 39 | # token: ${{secrets.CODECOV_TOKEN}} 40 | # file: ./client/coverage/coverage-final.json 41 | # flags: unittests 42 | # name: codecov-umbrella 43 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [ master ] 9 | 10 | jobs: 11 | analyze: 12 | name: Analyze 13 | runs-on: ubuntu-latest 14 | 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | language: [ 'go', 'typescript' ] 19 | 20 | steps: 21 | - name: Checkout repository 22 | uses: actions/checkout@v4.1.0 23 | 24 | # Initializes the CodeQL tools for scanning. 25 | - name: Initialize CodeQL 26 | uses: github/codeql-action/init@v2 27 | with: 28 | languages: ${{ matrix.language }} 29 | # If you wish to specify custom queries, you can do so here or in a config file. 30 | # By default, queries listed here will override any specified in a config file. 31 | # Prefix the list here with "+" to use these queries and those in the config file. 32 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 33 | 34 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 35 | # If this step fails, then you should remove it and run the build manually (see below) 36 | - name: Autobuild 37 | uses: github/codeql-action/autobuild@v2 38 | 39 | # ℹ️ Command-line programs to run using the OS shell. 40 | # 📚 https://git.io/JvXDl 41 | 42 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 43 | # and modify them (or add more) to build your code if your project 44 | # uses a compiled language 45 | 46 | #- run: | 47 | # make bootstrap 48 | # make release 49 | 50 | - name: Perform CodeQL Analysis 51 | uses: github/codeql-action/analyze@v2 52 | -------------------------------------------------------------------------------- /.github/workflows/dockerimage-next.yml: -------------------------------------------------------------------------------- 1 | name: Docker Push latest 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | if: contains(toJson(github.event.commits), '[action]') == false 12 | steps: 13 | - uses: actions/checkout@v4.1.0 14 | with: 15 | fetch-depth: 0 16 | 17 | - name: Set up QEMU 18 | uses: docker/setup-qemu-action@v2 19 | 20 | - name: Set up Docker Buildx 21 | uses: docker/setup-buildx-action@v2 22 | 23 | - name: Login to DockerHub 24 | uses: docker/login-action@v2.2.0 25 | with: 26 | username: ${{ secrets.DOCKER_USERNAME }} 27 | password: ${{ secrets.DOCKER_PASSWORD }} 28 | 29 | - name: Build and push 30 | id: docker_build 31 | uses: docker/build-push-action@v4.1.1 32 | with: 33 | context: . 34 | push: true 35 | tags: sundowndev/phoneinfoga:next 36 | platforms: linux/amd64 37 | 38 | - name: Display new image digest 39 | run: echo "sundowndev/phoneinfoga@${{ steps.docker_build.outputs.digest }}" 40 | -------------------------------------------------------------------------------- /.github/workflows/homebrew.yml: -------------------------------------------------------------------------------- 1 | name: Homebrew Bump Formula 2 | on: 3 | release: 4 | types: [published] 5 | workflow_dispatch: 6 | jobs: 7 | homebrew: 8 | runs-on: macos-latest 9 | steps: 10 | - uses: dawidd6/action-homebrew-bump-formula@v3 11 | with: 12 | token: ${{ secrets.HOMEBREW_GITHUB_API_TOKEN }} 13 | formula: phoneinfoga 14 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | goreleaser: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v4.1.0 14 | 15 | - name: Unshallow 16 | run: git fetch --prune --unshallow 17 | 18 | - name: Use Node.js ${{ matrix.node-version }} 19 | uses: actions/setup-node@v3.8.1 20 | with: 21 | node-version: 20.3.1 22 | 23 | - name: Set up Go 24 | uses: actions/setup-go@v4.1.0 25 | with: 26 | go-version: 1.20.6 27 | 28 | - name: Import GPG key 29 | id: import_gpg 30 | uses: crazy-max/ghaction-import-gpg@v6 31 | with: 32 | gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} 33 | passphrase: ${{ secrets.GPG_PASSPHRASE }} 34 | 35 | - name: Building static assets 36 | run: (cd web/client && yarn install --immutable && yarn build) 37 | 38 | - name: Install tools 39 | run: make install-tools 40 | 41 | - name: Run GoReleaser 42 | uses: goreleaser/goreleaser-action@v4.4.0 43 | with: 44 | version: v1.10.2 45 | args: release --rm-dist 46 | env: 47 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 48 | GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }} 49 | docker: 50 | runs-on: ubuntu-latest 51 | if: contains(toJson(github.event.commits), '[action]') == false 52 | steps: 53 | - uses: actions/checkout@v4.1.0 54 | with: 55 | fetch-depth: 0 56 | - name: Set up QEMU 57 | uses: docker/setup-qemu-action@v2 58 | - name: Set up Docker Buildx 59 | uses: docker/setup-buildx-action@v2 60 | - name: Login to DockerHub 61 | uses: docker/login-action@v2.2.0 62 | with: 63 | username: ${{ secrets.DOCKER_USERNAME }} 64 | password: ${{ secrets.DOCKER_PASSWORD }} 65 | 66 | - name: Build and push 67 | id: docker_build 68 | uses: docker/build-push-action@v4.1.1 69 | with: 70 | context: . 71 | push: true 72 | tags: sundowndev/phoneinfoga:latest,sundowndev/phoneinfoga:v2,sundowndev/phoneinfoga:stable,sundowndev/phoneinfoga:${{ github.ref_name }} 73 | platforms: linux/amd64 # ,linux/arm/v7,linux/arm64 - TODO(sundowndev): enable arm support back 74 | publish-docs: 75 | runs-on: ubuntu-latest 76 | steps: 77 | - uses: actions/checkout@v4.1.0 78 | - name: Set up Python 3.8 79 | uses: actions/setup-python@v4.7.0 80 | with: 81 | python-version: 3.8 82 | 83 | - name: Install dependencies 84 | run: | 85 | python -m pip install --upgrade pip 86 | python -m pip install mkdocs==1.3.0 mkdocs-material==8.3.9 mkdocs-minify-plugin==0.5.0 mkdocs-redirects==1.1.0 87 | 88 | - name: Deploy 89 | run: | 90 | git remote set-url origin https://${{ secrets.GITHUB_USER }}:${{ secrets.GITHUB_TOKEN }}@github.com/${{ secrets.GITHUB_USER }}/phoneinfoga.git 91 | git config --global user.email "${{ secrets.GITHUB_USER }}@users.noreply.github.com" 92 | git config --global user.name "${{ secrets.GITHUB_USER }}" 93 | mkdocs gh-deploy --force 94 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | /bin 8 | 9 | # Test binary, built with `go test -c` 10 | *.test 11 | 12 | # Output of the go coverage tool, specifically when used with LiteIDE 13 | *.out 14 | 15 | # Dependency directories (remove the comment below to include it) 16 | # vendor/ 17 | .vscode/ 18 | 19 | .DS_Store 20 | coverage 21 | coverage.* 22 | unit-tests.xml 23 | .idea 24 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | before: 2 | hooks: 3 | - go generate ./... 4 | - go mod download 5 | signs: 6 | - artifacts: checksum 7 | args: ["--batch", "-u", "{{ .Env.GPG_FINGERPRINT }}", "--output", "${signature}", "--detach-sign", "${artifact}"] 8 | signature: "${artifact}.gpg" 9 | builds: 10 | - env: 11 | - CGO_ENABLED=0 12 | - GO111MODULE=on 13 | binary: phoneinfoga 14 | goos: 15 | - linux 16 | - darwin 17 | - windows 18 | goarch: 19 | - 386 20 | - amd64 21 | - arm 22 | - arm64 23 | goarm: 24 | - 6 25 | - 7 26 | ldflags: -s -w -X github.com/sundowndev/phoneinfoga/v2/build.Version={{.Version}} -X github.com/sundowndev/phoneinfoga/v2/build.Commit={{.ShortCommit}} 27 | archives: 28 | - name_template: '{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}' 29 | replacements: 30 | darwin: Darwin 31 | linux: Linux 32 | windows: Windows 33 | 386: i386 34 | amd64: x86_64 35 | files: 36 | - none* 37 | checksum: 38 | name_template: '{{ .ProjectName }}_checksums.txt' 39 | snapshot: 40 | name_template: "{{ .Tag }}-next" 41 | changelog: 42 | sort: asc 43 | filters: 44 | exclude: 45 | - '^docs:' 46 | - '^test:' 47 | - '^chore:' 48 | - '^ci:' 49 | - Merge pull request 50 | - Merge branch 51 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @sundowndev 2 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | raphael[at]crvx.fr. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20.9.0-alpine AS client_builder 2 | 3 | WORKDIR /app 4 | 5 | COPY ./web/client . 6 | RUN yarn install --immutable 7 | RUN yarn build 8 | RUN yarn cache clean 9 | 10 | FROM golang:1.20.6-alpine AS go_builder 11 | 12 | WORKDIR /app 13 | 14 | RUN apk add --update --no-cache git make bash build-base 15 | COPY . . 16 | COPY --from=client_builder /app/dist ./web/client/dist 17 | RUN go get -v -t -d ./... 18 | RUN make install-tools 19 | RUN make build 20 | 21 | FROM alpine:3.18 22 | COPY --from=go_builder /app/bin/phoneinfoga /app/phoneinfoga 23 | EXPOSE 5000 24 | ENTRYPOINT ["/app/phoneinfoga"] 25 | CMD ["--help"] 26 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Use bash syntax 2 | SHELL=/bin/bash 3 | # Go parameters 4 | GOCMD=go 5 | GOBINPATH=$(shell $(GOCMD) env GOPATH)/bin 6 | GOMOD=$(GOCMD) mod 7 | GOBUILD=$(GOCMD) build 8 | GOCLEAN=$(GOCMD) clean 9 | GOTEST=gotestsum 10 | GOGET=$(GOCMD) get 11 | GOINSTALL=$(GOCMD) install 12 | GOTOOL=$(GOCMD) tool 13 | GOFMT=$(GOCMD) fmt 14 | GIT_TAG=$(shell git describe --abbrev=0 --tags) 15 | GIT_COMMIT=$(shell git rev-parse --short HEAD) 16 | 17 | .PHONY: FORCE 18 | 19 | .PHONY: all 20 | all: fmt lint test build go.mod 21 | 22 | .PHONY: build 23 | build: 24 | go generate ./... 25 | go build -v -ldflags="-s -w -X 'github.com/sundowndev/phoneinfoga/v2/build.Version=${GIT_TAG}' -X 'github.com/sundowndev/phoneinfoga/v2/build.Commit=${GIT_COMMIT}'" -o ./bin/phoneinfoga . 26 | 27 | .PHONY: test 28 | test: 29 | $(GOTEST) --format testname --junitfile unit-tests.xml -- -mod=readonly -race -coverprofile=./c.out -covermode=atomic -coverpkg=.,./... ./... 30 | 31 | .PHONY: coverage 32 | coverage: test 33 | $(GOTOOL) cover -func=cover.out 34 | 35 | .PHONY: mocks 36 | mocks: 37 | rm -rf mocks 38 | mockery --all 39 | 40 | .PHONY: fmt 41 | fmt: 42 | $(GOFMT) ./... 43 | 44 | .PHONY: clean 45 | clean: 46 | $(GOCLEAN) 47 | rm -f bin/* 48 | 49 | .PHONY: lint 50 | lint: 51 | golangci-lint run -v --timeout=2m 52 | 53 | .PHONY: install-tools 54 | install-tools: 55 | $(GOINSTALL) gotest.tools/gotestsum@v1.6.3 56 | $(GOINSTALL) github.com/vektra/mockery/v2@v2.38.0 57 | $(GOINSTALL) github.com/swaggo/swag/cmd/swag@v1.16.3 58 | @which golangci-lint > /dev/null 2>&1 || (curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | bash -s -- -b $(GOBINPATH) v1.46.2) 59 | 60 | go.mod: FORCE 61 | $(GOMOD) tidy 62 | $(GOMOD) verify 63 | go.sum: go.mod 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | <p align="center"> 2 | <img src="./docs/images/banner.png" width=500 alt="project logo"/> 3 | </p> 4 | 5 | <div align="center"> 6 | <a href="https://github.com/sundowndev/phoneinfoga/actions"> 7 | <img src="https://github.com/sundowndev/phoneinfoga/workflows/Build/badge.svg" alt="build status" /> 8 | </a> 9 | <a href="https://goreportcard.com/report/github.com/sundowndev/phoneinfoga/v2"> 10 | <img src="https://goreportcard.com/badge/github.com/sundowndev/phoneinfoga/v2" alt="go report" /> 11 | </a> 12 | <a href="https://codeclimate.com/github/sundowndev/phoneinfoga/maintainability"> 13 | <img src="https://api.codeclimate.com/v1/badges/3259feb1c68df1cd4f71/maintainability" alt="code climate badge"/> 14 | </a> 15 | <a href='https://coveralls.io/github/sundowndev/phoneinfoga'> 16 | <img src='https://coveralls.io/repos/github/sundowndev/phoneinfoga/badge.svg' alt='Coverage Status' /> 17 | </a> 18 | <a href="https://github.com/sundowndev/phoneinfoga/releases"> 19 | <img src="https://img.shields.io/github/release/SundownDEV/phoneinfoga.svg" alt="Latest version" /> 20 | </a> 21 | <a href="https://hub.docker.com/r/sundowndev/phoneinfoga"> 22 | <img src="https://img.shields.io/docker/pulls/sundowndev/phoneinfoga.svg" alt="Docker pulls" /> 23 | </a> 24 | </div> 25 | 26 | <h4 align="center">Information gathering framework for phone numbers</h4> 27 | 28 | <p align="center"> 29 | <a href="https://sundowndev.github.io/phoneinfoga/">Documentation</a> • 30 | <a href="https://petstore.swagger.io/?url=https://raw.githubusercontent.com/sundowndev/phoneinfoga/master/web/docs/swagger.yaml">API documentation</a> • 31 | <a href="https://medium.com/@SundownDEV/phone-number-scanning-osint-recon-tool-6ad8f0cac27b">Related blog post</a> 32 | </p> 33 | 34 | ## About 35 | 36 | PhoneInfoga is one of the most advanced tools to scan international phone numbers. It allows you to first gather basic information such as country, area, carrier and line type, then use various techniques to try to find the VoIP provider or identify the owner. It works with a collection of scanners that must be configured in order for the tool to be effective. PhoneInfoga doesn't automate everything, it's just there to help investigating on phone numbers. 37 | 38 | ## Current status 39 | 40 | This project is stable and production-ready. 41 | 42 | #### Demo instance termination 43 | 44 | The demo instance has been terminated on December 21th, 2023. It's been expensive to maintain this instance throughout the years given the number of requests it received (~20K/month), without bringing much value to users compared to using it locally. Use it locally with your own API keys for a better experience. 45 | 46 | ## Features 47 | 48 | - Check if phone number exists 49 | - Gather basic information such as country, line type and carrier 50 | - OSINT footprinting using external APIs, phone books & search engines 51 | - Check for reputation reports, social media, disposable numbers and more 52 | - Use the graphical user interface to run scans from the browser 53 | - Programmatic usage with the [REST API](https://petstore.swagger.io/?url=https://raw.githubusercontent.com/sundowndev/phoneinfoga/master/web/docs/swagger.yaml) and [Go modules](https://pkg.go.dev/github.com/sundowndev/phoneinfoga/v2) 54 | 55 | ## Anti-features 56 | 57 | - Does not claim to provide relevant or verified data, it's just a tool ! 58 | - Does not allow to "track" a phone or its owner in real time 59 | - Does not allow to get the precise phone location 60 | - Does not allow to hack a phone 61 | 62 | ## License 63 | 64 | [](https://app.fossa.com/projects/git%2Bgithub.com%2Fsundowndev%2FPhoneInfoga?ref=badge_shield) 65 | 66 | This tool is licensed under the GNU General Public License v3.0. 67 | 68 | [Icon](https://www.flaticon.com/free-icon/fingerprint-search-symbol-of-secret-service-investigation_48838) made by <a href="https://www.freepik.com/" title="Freepik">Freepik</a> from <a href="https://www.flaticon.com/" title="Flaticon">flaticon.com</a> is licensed by <a href="http://creativecommons.org/licenses/by/3.0/" title="Creative Commons BY 3.0" target="_blank">CC 3.0 BY</a>. 69 | 70 | ## Support 71 | 72 | Support me by signing up to DigitalOcean using my link ($200 free credits) 73 | 74 | [](https://www.digitalocean.com/?refcode=31f5ef768eb3&utm_campaign=Referral_Invite&utm_medium=Referral_Program&utm_source=badge) 75 | 76 | <div align="center"> 77 | <img src="https://github.com/sundowndev/static/raw/main/sponsors.svg?v=c68eba9" width="100%" heigh="auto" /> 78 | </div> 79 | -------------------------------------------------------------------------------- /bin/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sundowndev/phoneinfoga/5f6156f567613a07ea8e3f04b004864582060a57/bin/.gitkeep -------------------------------------------------------------------------------- /build/build.go: -------------------------------------------------------------------------------- 1 | package build 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | ) 7 | 8 | // Version is the corresponding release tag 9 | var Version = "dev" 10 | 11 | // Commit is the corresponding Git commit 12 | var Commit = "dev" 13 | 14 | func IsRelease() bool { 15 | return String() != "dev-dev" 16 | } 17 | 18 | func String() string { 19 | return fmt.Sprintf("%s-%s", Version, Commit) 20 | } 21 | 22 | func IsDemo() bool { 23 | return os.Getenv("PHONEINFOGA_DEMO") == "true" 24 | } 25 | -------------------------------------------------------------------------------- /build/build_test.go: -------------------------------------------------------------------------------- 1 | package build 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "os" 6 | "testing" 7 | ) 8 | 9 | func TestBuild(t *testing.T) { 10 | t.Run("version and commit default values", func(t *testing.T) { 11 | assert.Equal(t, "dev", Version) 12 | assert.Equal(t, "dev", Commit) 13 | assert.Equal(t, false, IsRelease()) 14 | assert.Equal(t, "dev-dev", String()) 15 | assert.Equal(t, false, IsDemo()) 16 | }) 17 | 18 | t.Run("version and commit default values", func(t *testing.T) { 19 | Version = "v2.4.4" 20 | Commit = "0ba854f" 21 | _ = os.Setenv("PHONEINFOGA_DEMO", "true") 22 | defer os.Unsetenv("PHONEINFOGA_DEMO") 23 | 24 | assert.Equal(t, true, IsRelease()) 25 | assert.Equal(t, "v2.4.4-0ba854f", String()) 26 | assert.Equal(t, true, IsDemo()) 27 | }) 28 | } 29 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "github.com/fatih/color" 6 | "os" 7 | 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | var rootCmd = &cobra.Command{ 12 | Use: "phoneinfoga [COMMANDS] [OPTIONS]", 13 | Short: "Advanced information gathering & OSINT tool for phone numbers", 14 | Long: "PhoneInfoga is one of the most advanced tools to scan phone numbers using only free resources.", 15 | Example: "phoneinfoga scan -n <number>", 16 | } 17 | 18 | // Execute is a function that executes the root command 19 | func Execute() { 20 | if err := rootCmd.Execute(); err != nil { 21 | exitWithError(err) 22 | } 23 | } 24 | 25 | func exitWithError(err error) { 26 | fmt.Fprintf(color.Error, "%s\n", color.RedString(err.Error())) 27 | os.Exit(1) 28 | } 29 | -------------------------------------------------------------------------------- /cmd/scan.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/fatih/color" 7 | "github.com/joho/godotenv" 8 | "github.com/sirupsen/logrus" 9 | "github.com/spf13/cobra" 10 | "github.com/sundowndev/phoneinfoga/v2/lib/filter" 11 | "github.com/sundowndev/phoneinfoga/v2/lib/number" 12 | "github.com/sundowndev/phoneinfoga/v2/lib/output" 13 | "github.com/sundowndev/phoneinfoga/v2/lib/remote" 14 | ) 15 | 16 | type ScanCmdOptions struct { 17 | Number string 18 | DisabledScanners []string 19 | PluginPaths []string 20 | EnvFiles []string 21 | } 22 | 23 | func init() { 24 | // Register command 25 | opts := &ScanCmdOptions{} 26 | cmd := NewScanCmd(opts) 27 | rootCmd.AddCommand(cmd) 28 | 29 | // Register flags 30 | cmd.PersistentFlags().StringVarP(&opts.Number, "number", "n", "", "The phone number to scan (E164 or international format)") 31 | cmd.PersistentFlags().StringArrayVarP(&opts.DisabledScanners, "disable", "D", []string{}, "Scanner to skip for this scan") 32 | cmd.PersistentFlags().StringArrayVar(&opts.PluginPaths, "plugin", []string{}, "Extra scanner plugin to use for the scan") 33 | cmd.PersistentFlags().StringSliceVar(&opts.EnvFiles, "env-file", []string{}, "Env files to parse environment variables from (looks for .env by default)") 34 | // scanCmd.PersistentFlags().StringVarP(&input, "input", "i", "", "Text file containing a list of phone numbers to scan (one per line)") 35 | // scanCmd.PersistentFlags().StringVarP(&output, "output", "o", "", "Output to save scan results") 36 | } 37 | 38 | func NewScanCmd(opts *ScanCmdOptions) *cobra.Command { 39 | return &cobra.Command{ 40 | Use: "scan", 41 | Short: "Scan a phone number", 42 | Args: cobra.NoArgs, 43 | Run: func(cmd *cobra.Command, args []string) { 44 | err := godotenv.Load(opts.EnvFiles...) 45 | if err != nil { 46 | logrus.WithField("error", err).Debug("Error loading .env file") 47 | } 48 | 49 | runScan(opts) 50 | }, 51 | } 52 | } 53 | 54 | func runScan(opts *ScanCmdOptions) { 55 | fmt.Fprintf(color.Output, color.WhiteString("Running scan for phone number %s...\n\n"), opts.Number) 56 | 57 | if valid := number.IsValid(opts.Number); !valid { 58 | logrus.WithFields(map[string]interface{}{ 59 | "input": opts.Number, 60 | "valid": valid, 61 | }).Debug("Input phone number is invalid") 62 | exitWithError(errors.New("given phone number is not valid")) 63 | } 64 | 65 | num, err := number.NewNumber(opts.Number) 66 | if err != nil { 67 | exitWithError(err) 68 | } 69 | 70 | for _, p := range opts.PluginPaths { 71 | err := remote.OpenPlugin(p) 72 | if err != nil { 73 | exitWithError(err) 74 | } 75 | } 76 | 77 | f := filter.NewEngine() 78 | f.AddRule(opts.DisabledScanners...) 79 | 80 | remoteLibrary := remote.NewLibrary(f) 81 | remote.InitScanners(remoteLibrary) 82 | 83 | // Scanner options are currently not used in CLI 84 | result, errs := remoteLibrary.Scan(num, remote.ScannerOptions{}) 85 | 86 | err = output.GetOutput(output.Console, color.Output).Write(result, errs) 87 | if err != nil { 88 | exitWithError(err) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /cmd/scanners.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "github.com/spf13/cobra" 6 | "github.com/sundowndev/phoneinfoga/v2/lib/filter" 7 | "github.com/sundowndev/phoneinfoga/v2/lib/remote" 8 | ) 9 | 10 | type ScannersCmdOptions struct { 11 | Plugin []string 12 | } 13 | 14 | func init() { 15 | opts := &ScannersCmdOptions{} 16 | scannersCmd := NewScannersCmd(opts) 17 | 18 | fl := scannersCmd.Flags() 19 | fl.StringSliceVar(&opts.Plugin, "plugin", []string{}, "Output file") 20 | 21 | rootCmd.AddCommand(scannersCmd) 22 | } 23 | 24 | func NewScannersCmd(opts *ScannersCmdOptions) *cobra.Command { 25 | cmd := &cobra.Command{ 26 | Use: "scanners", 27 | Example: "phoneinfoga scanners", 28 | Short: "Display list of loaded scanners", 29 | Run: func(cmd *cobra.Command, args []string) { 30 | for _, p := range opts.Plugin { 31 | err := remote.OpenPlugin(p) 32 | if err != nil { 33 | exitWithError(err) 34 | } 35 | } 36 | 37 | remoteLibrary := remote.NewLibrary(filter.NewEngine()) 38 | remote.InitScanners(remoteLibrary) 39 | 40 | for i, s := range remoteLibrary.GetAllScanners() { 41 | fmt.Printf("%s\n%s\n", s.Name(), s.Description()) 42 | if i < len(remoteLibrary.GetAllScanners()) { 43 | fmt.Printf("\n") 44 | } 45 | } 46 | }, 47 | } 48 | return cmd 49 | } 50 | -------------------------------------------------------------------------------- /cmd/serve.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "github.com/gin-gonic/gin" 6 | "github.com/joho/godotenv" 7 | "github.com/sirupsen/logrus" 8 | "github.com/spf13/cobra" 9 | "github.com/sundowndev/phoneinfoga/v2/build" 10 | "github.com/sundowndev/phoneinfoga/v2/lib/filter" 11 | "github.com/sundowndev/phoneinfoga/v2/lib/remote" 12 | "github.com/sundowndev/phoneinfoga/v2/web" 13 | "github.com/sundowndev/phoneinfoga/v2/web/v2/api/handlers" 14 | "log" 15 | "net/http" 16 | "os" 17 | ) 18 | 19 | type ServeCmdOptions struct { 20 | HttpPort int 21 | DisableClient bool 22 | DisabledScanners []string 23 | PluginPaths []string 24 | EnvFiles []string 25 | } 26 | 27 | func init() { 28 | // Register command 29 | opts := &ServeCmdOptions{} 30 | cmd := NewServeCmd(opts) 31 | rootCmd.AddCommand(cmd) 32 | 33 | // Register flags 34 | cmd.PersistentFlags().IntVarP(&opts.HttpPort, "port", "p", 5000, "HTTP port") 35 | cmd.PersistentFlags().BoolVar(&opts.DisableClient, "no-client", false, "Disable web client (REST API only)") 36 | cmd.PersistentFlags().StringArrayVarP(&opts.DisabledScanners, "disable", "D", []string{}, "Scanner to skip for the scans") 37 | cmd.PersistentFlags().StringArrayVar(&opts.PluginPaths, "plugin", []string{}, "Extra scanner plugin to use for the scans") 38 | cmd.PersistentFlags().StringSliceVar(&opts.EnvFiles, "env-file", []string{}, "Env files to parse environment variables from (looks for .env by default)") 39 | } 40 | 41 | func NewServeCmd(opts *ServeCmdOptions) *cobra.Command { 42 | return &cobra.Command{ 43 | Use: "serve", 44 | Short: "Serve web client", 45 | PreRun: func(cmd *cobra.Command, args []string) { 46 | err := godotenv.Load(opts.EnvFiles...) 47 | if err != nil { 48 | logrus.WithField("error", err).Debug("Error loading .env file") 49 | } 50 | 51 | for _, p := range opts.PluginPaths { 52 | err := remote.OpenPlugin(p) 53 | if err != nil { 54 | exitWithError(err) 55 | } 56 | } 57 | 58 | // Initialize remote library 59 | f := filter.NewEngine() 60 | f.AddRule(opts.DisabledScanners...) 61 | handlers.Init(f) 62 | }, 63 | Run: func(cmd *cobra.Command, args []string) { 64 | if build.IsRelease() && os.Getenv("GIN_MODE") == "" { 65 | gin.SetMode(gin.ReleaseMode) 66 | } 67 | 68 | srv, err := web.NewServer(opts.DisableClient) 69 | if err != nil { 70 | log.Fatal(err) 71 | } 72 | 73 | addr := fmt.Sprintf(":%d", opts.HttpPort) 74 | fmt.Printf("Listening on %s\n", addr) 75 | if err := srv.ListenAndServe(addr); err != nil && err != http.ErrServerClosed { 76 | log.Fatalf("listen: %s\n", err) 77 | } 78 | }, 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "github.com/spf13/cobra" 6 | "github.com/sundowndev/phoneinfoga/v2/build" 7 | ) 8 | 9 | func init() { 10 | rootCmd.AddCommand(versionCmd) 11 | } 12 | 13 | var versionCmd = &cobra.Command{ 14 | Use: "version", 15 | Short: "Print current version of the tool", 16 | Run: func(cmd *cobra.Command, args []string) { 17 | fmt.Printf("PhoneInfoga %s\n", build.String()) 18 | }, 19 | } 20 | -------------------------------------------------------------------------------- /docs/contribute.md: -------------------------------------------------------------------------------- 1 | --- 2 | hide: 3 | - navigation 4 | --- 5 | 6 | # Contribute 7 | 8 | This page describe the project structure and gives you a bit of context to start contributing to the project. 9 | 10 | ## Project 11 | 12 | ### Building from source 13 | 14 | **Requirements :** 15 | 16 | - Nodejs >= v15 17 | - npm or yarn 18 | - Go >= 1.16 19 | 20 | **Note:** if you're using npm, just replace `yarn <command>` by `npm run <command>`. 21 | 22 | ```shell 23 | # Install tools needed to build, creating mocks or running tests 24 | $ make install-tools 25 | 26 | # Build static assets 27 | # This will create dist directory containing client's static files 28 | $ (cd web/client && yarn && yarn build) 29 | 30 | # Generate in-memory assets, then build the project. 31 | # This will put content of dist directory in a single binary file. 32 | # It's needed to build but the design requires you to do it anyway. 33 | # This step is needed at each change if you're developing on the client. 34 | $ make build 35 | ``` 36 | 37 | If you're developing, you don't need to build at each changes, you can compile then run with the `go run` command : 38 | 39 | ``` 40 | $ go run main.go 41 | ``` 42 | 43 | ### File structure 44 | 45 | ```shell 46 | bin/ # Local binaries 47 | build/ # Build package providing info about the current build 48 | cmd/ # Command-line app code 49 | docs/ # Documentation 50 | examples/ # Some code examples 51 | lib/ # Libraries 52 | mocks/ # Mocks 53 | support/ # Utilities, manifests for production and local env 54 | test/ # Utilities for testing purposes 55 | web/ # Web server, including REST API and web client 56 | go.mod # Go modules file 57 | main.go # Application entrypoint 58 | ``` 59 | 60 | ## Testing 61 | 62 | ### Go code 63 | 64 | ```shell 65 | # Run test suite 66 | go test -v ./... 67 | 68 | # Collect coverage 69 | go test -coverprofile=coverage.out ./... 70 | 71 | # Open coverage file as HTML 72 | go tool cover -html=coverage.out 73 | ``` 74 | 75 | ### Typescript code 76 | 77 | Developping on the web client. 78 | 79 | ```shell 80 | cd web/client 81 | 82 | yarn test 83 | yarn test:unit 84 | yarn test:e2e 85 | ``` 86 | 87 | If you're developing on the client, you can watch changes with `yarn build:watch`. 88 | 89 | ## Formatting 90 | 91 | ### Go code 92 | 93 | We use gofmt to format Go files. 94 | 95 | ```shell 96 | make fmt 97 | ``` 98 | 99 | ### Typescript code 100 | 101 | ```shell 102 | cd web/client 103 | 104 | yarn lint 105 | yarn lint:fix 106 | ``` 107 | 108 | ## Documentation 109 | 110 | We use [mkdocs](https://www.mkdocs.org/) to generate our documentation website. 111 | 112 | ### Install mkdocs 113 | 114 | ```shell 115 | python3 -m pip install mkdocs==1.3.0 mkdocs-material==8.3.9 mkdocs-minify-plugin==0.5.0 mkdocs-redirects==1.1.0 116 | ``` 117 | 118 | ### Serve documentation on localhost 119 | 120 | This is the only command you need to start working on docs. 121 | 122 | ```shell 123 | mkdocs serve 124 | # or 125 | python3 -m mkdocs serve 126 | ``` 127 | 128 | ### Build website 129 | 130 | ```shell 131 | mkdocs build 132 | ``` 133 | 134 | ### Deploy on github pages 135 | 136 | ```shell 137 | mkdocs gh-deploy 138 | ``` 139 | -------------------------------------------------------------------------------- /docs/getting-started/go-module-usage.md: -------------------------------------------------------------------------------- 1 | # Go module usage 2 | 3 | You can easily use scanners in your own Golang script. You can find [Go documentation here](https://pkg.go.dev/github.com/sundowndev/phoneinfoga/v2). 4 | 5 | ### Install the module 6 | 7 | ``` 8 | go get -v github.com/sundowndev/phoneinfoga/v2 9 | ``` 10 | 11 | ### Usage example 12 | 13 | ```go 14 | package main 15 | 16 | import ( 17 | "fmt" 18 | "log" 19 | 20 | "github.com/sundowndev/phoneinfoga/v2/lib/number" 21 | "github.com/sundowndev/phoneinfoga/v2/lib/remote" 22 | ) 23 | 24 | func main() { 25 | n, err := number.NewNumber("...") 26 | if err != nil { 27 | log.Fatal(err) 28 | } 29 | 30 | res, err := remote.NewGoogleSearchScanner().Scan(n) 31 | if err != nil { 32 | log.Fatal(err) 33 | } 34 | 35 | links := res.(remote.GoogleSearchResponse) 36 | for _, link := range links.Individuals { 37 | fmt.Println(link.URL) // Google search link to scan 38 | } 39 | } 40 | ``` 41 | -------------------------------------------------------------------------------- /docs/getting-started/install.md: -------------------------------------------------------------------------------- 1 | To install PhoneInfoga, you'll need to download the binary or build the software from its source code. 2 | 3 | !!! info 4 | For now, only Linux, MacOS and Windows are supported. If you don't see your OS/arch on the [release page on GitHub](https://github.com/sundowndev/phoneinfoga/releases), it means it's not explicitly supported. You can build from source by yourself anyway. Want your OS to be supported ? Please [open an issue on GitHub](https://github.com/sundowndev/phoneinfoga/issues). 5 | 6 | ## Binary installation (recommended) 7 | 8 | Follow the instructions : 9 | 10 | - Go to [release page on GitHub](https://github.com/sundowndev/phoneinfoga/releases) 11 | - Choose your OS and architecture 12 | - Download the archive, extract the binary then run it in a terminal 13 | 14 | You can also do it from the terminal (UNIX systems only) : 15 | 16 | 1. Download the latest release in the current directory 17 | 18 | ``` 19 | # Add --help at the end of the command for a list of install options 20 | bash <( curl -sSL https://raw.githubusercontent.com/sundowndev/phoneinfoga/master/support/scripts/install ) 21 | ``` 22 | 23 | 2. Install it globally 24 | ``` 25 | sudo install ./phoneinfoga /usr/local/bin/phoneinfoga 26 | ``` 27 | 28 | 3. Test to ensure the version you installed is up-to-date 29 | ``` 30 | ./phoneinfoga version 31 | ``` 32 | 33 | To ensure your system is supported, please check the output of `echo "$(uname -s)_$(uname -m)"` in your terminal and see if it's available on the [GitHub release page](https://github.com/sundowndev/phoneinfoga/releases). 34 | 35 | ## Homebrew 36 | 37 | PhoneInfoga is now available on Homebrew. Homebrew is a free and open-source package management system for Mac OS X. Install the official phoneinfoga formula from the terminal. 38 | 39 | ```shell 40 | brew install phoneinfoga 41 | ``` 42 | 43 | ## Docker 44 | 45 | !!! info 46 | If you want to use the beta channel, you can use the `next` tag, it's updated directly from the master branch. But in most cases we recommend using [`latest`, `v2` or `stable` tags](https://hub.docker.com/r/sundowndev/phoneinfoga/tags) to only get release updates. 47 | 48 | ### From docker hub 49 | 50 | You can pull the repository directly from Docker hub 51 | 52 | ```shell 53 | docker pull sundowndev/phoneinfoga:latest 54 | ``` 55 | 56 | Then run the tool 57 | 58 | ```shell 59 | docker run --rm -it sundowndev/phoneinfoga version 60 | ``` 61 | 62 | ### Docker-compose 63 | 64 | You can use a single docker-compose file to run the tool without downloading the source code. 65 | 66 | ``` 67 | version: '3.7' 68 | 69 | services: 70 | phoneinfoga: 71 | container_name: phoneinfoga 72 | restart: on-failure 73 | image: sundowndev/phoneinfoga:latest 74 | command: 75 | - "serve" 76 | ports: 77 | - "80:5000" 78 | ``` 79 | 80 | ### Build from source 81 | 82 | You can download the source code, then build the docker images 83 | 84 | #### Build 85 | 86 | Build the image 87 | 88 | ```shell 89 | docker-compose build 90 | ``` 91 | 92 | #### CLI usage 93 | 94 | ```shell 95 | docker-compose run --rm phoneinfoga --help 96 | ``` 97 | 98 | #### Run web services 99 | 100 | ```shell 101 | docker-compose up -d 102 | ``` 103 | 104 | ##### Disable web client 105 | 106 | Edit `docker-compose.yml` and add the `--no-client` option 107 | 108 | ```yaml 109 | # docker-compose.yml 110 | command: 111 | - "serve" 112 | - "--no-client" 113 | ``` 114 | 115 | #### Troubleshooting 116 | 117 | All the output is sent to stdout, so it can be inspected by running: 118 | 119 | ```shell 120 | docker logs -f <container-id|container-name> 121 | ``` 122 | -------------------------------------------------------------------------------- /docs/getting-started/usage.md: -------------------------------------------------------------------------------- 1 | ### Running a scan 2 | 3 | Use the `scan` command with the `-n` (or `--number`) option. 4 | 5 | ``` 6 | phoneinfoga scan -n "+1 (555) 444-1212" 7 | phoneinfoga scan -n "+33 06 79368229" 8 | phoneinfoga scan -n "33679368229" 9 | ``` 10 | 11 | Special chars such as `( ) - +` will be escaped so typing US-based numbers stay easy : 12 | 13 | ``` 14 | phoneinfoga scan -n "+1 555-444-3333" 15 | ``` 16 | 17 | !!! note "Note that the country code is essential. You don't know which country code to use ? [Find it here](https://www.countrycode.org/)" 18 | 19 | <!-- 20 | #### Input & output file 21 | 22 | Check several numbers at once and send results to a file. 23 | 24 | ``` 25 | phoneinfoga scan -i numbers.txt -o results.txt 26 | ``` 27 | 28 | Input file must contain one phone number per line. Invalid numbers will be skipped. 29 | 30 | #### Footprinting 31 | 32 | ``` 33 | phoneinfoga scan -n +42837544833 -s footprints 34 | ``` 35 | 36 | #### Custom format reconnaissance 37 | 38 | You don't know where to search and what custom format to use ? Let the tool try several custom formats based on the country code for you. 39 | 40 | ``` 41 | phoneinfoga recon -n +42837544833 42 | ``` 43 | --> 44 | 45 | ## Available scanners 46 | 47 | PhoneInfoga embed a bunch of scanners that will provide information about the given phone number. Some of them will request external services, and so might require authentication. By default, unconfigured scanners won't run. The information gathered can then be used for a deeper manual analysis. 48 | 49 | See page related to [scanners](scanners.md). 50 | 51 | ## Launching the web server 52 | 53 | PhoneInfoga integrates a REST API along with a web client that you can deploy anywhere. The API has been written in Go and web client in Vue.js. The application is stateless, so it doesn't require any persistent storage. 54 | 55 | See **[API documentation](https://petstore.swagger.io/?url=https://raw.githubusercontent.com/sundowndev/phoneinfoga/master/web/docs/swagger.yaml)**. 56 | 57 | ```shell 58 | phoneinfoga serve # uses default port 5000 59 | phoneinfoga serve -p 8080 # use port 8080 60 | ``` 61 | 62 | Equivalent commands via docker: 63 | 64 | ```shell 65 | docker run --rm -it -p 5000:5000 sundowndev/phoneinfoga serve # same as `phoneinfoga serve` 66 | docker run --rm -it -p 8080:8080 sundowndev/phoneinfoga serve -p 8080 # same as `phoneinfoga serve -p 8080` 67 | ``` 68 | 69 | You should then be able to visit the web client from your browser at `http://localhost:<port>`. 70 | 71 |  72 | 73 | **Running the REST API only** 74 | 75 | You can choose to only run the REST API without the web client: 76 | 77 | ```shell 78 | phoneinfoga serve --no-client 79 | ``` 80 | 81 | Equivalent docker command: 82 | 83 | ```shell 84 | docker run --rm -it -p 5000:5000 sundowndev/phoneinfoga serve --no-client 85 | ``` 86 | -------------------------------------------------------------------------------- /docs/images/0e2SMdL.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sundowndev/phoneinfoga/5f6156f567613a07ea8e3f04b004864582060a57/docs/images/0e2SMdL.png -------------------------------------------------------------------------------- /docs/images/KfrvacR.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sundowndev/phoneinfoga/5f6156f567613a07ea8e3f04b004864582060a57/docs/images/KfrvacR.png -------------------------------------------------------------------------------- /docs/images/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sundowndev/phoneinfoga/5f6156f567613a07ea8e3f04b004864582060a57/docs/images/banner.png -------------------------------------------------------------------------------- /docs/images/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sundowndev/phoneinfoga/5f6156f567613a07ea8e3f04b004864582060a57/docs/images/screenshot.png -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | hide: 3 | - navigation 4 | --- 5 | 6 | # Welcome to the PhoneInfoga documentation website 7 | 8 | PhoneInfoga is one of the most advanced tools to scan international phone numbers. It allows you to first gather standard information such as country, area, carrier and line type on any international phone number, then search for footprints on search engines to try to find the VoIP provider or identify the owner. 9 | 10 | [Read the related blog post](https://medium.com/@SundownDEV/phone-number-scanning-osint-recon-tool-6ad8f0cac27b){ .md-button .md-button--primary } 11 | 12 | ## Features 13 | 14 | - Check if phone number exists 15 | - Gather basic information such as country, line type and carrier 16 | - OSINT footprinting using external APIs, phone books & search engines 17 | - Check for reputation reports, social media, disposable numbers and more 18 | - Use the graphical user interface to run scans from the browser 19 | - Programmatic usage with the [REST API](https://petstore.swagger.io/?url=https://raw.githubusercontent.com/sundowndev/phoneinfoga/master/web/docs/swagger.yaml) and [Go modules](https://pkg.go.dev/github.com/sundowndev/phoneinfoga/v2) 20 | 21 | ## Anti-features 22 | 23 | - Does not claim to provide relevant or verified data, it's just a tool ! 24 | - Does not allow to "track" a phone or its owner in real time 25 | - Does not allow to get the precise phone location 26 | - Does not allow to hack a phone 27 | -------------------------------------------------------------------------------- /docs/jetbrains.svg: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> 3 | <svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" 4 | width="120.1px" height="130.2px" viewBox="0 0 120.1 130.2" style="enable-background:new 0 0 120.1 130.2;" xml:space="preserve" 5 | > 6 | <g> 7 | <linearGradient id="XMLID_2_" gradientUnits="userSpaceOnUse" x1="31.8412" y1="120.5578" x2="110.2402" y2="73.24"> 8 | <stop offset="0" style="stop-color:#FCEE39"/> 9 | <stop offset="1" style="stop-color:#F37B3D"/> 10 | </linearGradient> 11 | <path id="XMLID_3041_" style="fill:url(#XMLID_2_);" d="M118.6,71.8c0.9-0.8,1.4-1.9,1.5-3.2c0.1-2.6-1.8-4.7-4.4-4.9 12 | c-1.2-0.1-2.4,0.4-3.3,1.1l0,0l-83.8,45.9c-1.9,0.8-3.6,2.2-4.7,4.1c-2.9,4.8-1.3,11,3.6,13.9c3.4,2,7.5,1.8,10.7-0.2l0,0l0,0 13 | c0.2-0.2,0.5-0.3,0.7-0.5l78-54.8C117.3,72.9,118.4,72.1,118.6,71.8L118.6,71.8L118.6,71.8z"/> 14 | <linearGradient id="XMLID_3_" gradientUnits="userSpaceOnUse" x1="48.3607" y1="6.9083" x2="119.9179" y2="69.5546"> 15 | <stop offset="0" style="stop-color:#EF5A6B"/> 16 | <stop offset="0.57" style="stop-color:#F26F4E"/> 17 | <stop offset="1" style="stop-color:#F37B3D"/> 18 | </linearGradient> 19 | <path id="XMLID_3049_" style="fill:url(#XMLID_3_);" d="M118.8,65.1L118.8,65.1L55,2.5C53.6,1,51.6,0,49.3,0 20 | c-4.3,0-7.7,3.5-7.7,7.7v0c0,2.1,0.8,3.9,2.1,5.3l0,0l0,0c0.4,0.4,0.8,0.7,1.2,1l67.4,57.7l0,0c0.8,0.7,1.8,1.2,3,1.3 21 | c2.6,0.1,4.7-1.8,4.9-4.4C120.2,67.3,119.7,66,118.8,65.1z"/> 22 | <linearGradient id="XMLID_4_" gradientUnits="userSpaceOnUse" x1="52.9467" y1="63.6407" x2="10.5379" y2="37.1562"> 23 | <stop offset="0" style="stop-color:#7C59A4"/> 24 | <stop offset="0.3852" style="stop-color:#AF4C92"/> 25 | <stop offset="0.7654" style="stop-color:#DC4183"/> 26 | <stop offset="0.957" style="stop-color:#ED3D7D"/> 27 | </linearGradient> 28 | <path id="XMLID_3042_" style="fill:url(#XMLID_4_);" d="M57.1,59.5C57,59.5,17.7,28.5,16.9,28l0,0l0,0c-0.6-0.3-1.2-0.6-1.8-0.9 29 | c-5.8-2.2-12.2,0.8-14.4,6.6c-1.9,5.1,0.2,10.7,4.6,13.4l0,0l0,0C6,47.5,6.6,47.8,7.3,48c0.4,0.2,45.4,18.8,45.4,18.8l0,0 30 | c1.8,0.8,3.9,0.3,5.1-1.2C59.3,63.7,59,61,57.1,59.5z"/> 31 | <linearGradient id="XMLID_5_" gradientUnits="userSpaceOnUse" x1="52.1736" y1="3.7019" x2="10.7706" y2="37.8971"> 32 | <stop offset="0" style="stop-color:#EF5A6B"/> 33 | <stop offset="0.364" style="stop-color:#EE4E72"/> 34 | <stop offset="1" style="stop-color:#ED3D7D"/> 35 | </linearGradient> 36 | <path id="XMLID_3057_" style="fill:url(#XMLID_5_);" d="M49.3,0c-1.7,0-3.3,0.6-4.6,1.5L4.9,28.3c-0.1,0.1-0.2,0.1-0.2,0.2l-0.1,0 37 | l0,0c-1.7,1.2-3.1,3-3.9,5.1C-1.5,39.4,1.5,45.9,7.3,48c3.6,1.4,7.5,0.7,10.4-1.4l0,0l0,0c0.7-0.5,1.3-1,1.8-1.6l34.6-31.2l0,0 38 | c1.8-1.4,3-3.6,3-6.1v0C57.1,3.5,53.6,0,49.3,0z"/> 39 | <g id="XMLID_3008_"> 40 | <rect id="XMLID_3033_" x="34.6" y="37.4" style="fill:#000000;" width="51" height="51"/> 41 | <rect id="XMLID_3032_" x="39" y="78.8" style="fill:#FFFFFF;" width="19.1" height="3.2"/> 42 | <g id="XMLID_3009_"> 43 | <path id="XMLID_3030_" style="fill:#FFFFFF;" d="M38.8,50.8l1.5-1.4c0.4,0.5,0.8,0.8,1.3,0.8c0.6,0,0.9-0.4,0.9-1.2l0-5.3l2.3,0 44 | l0,5.3c0,1-0.3,1.8-0.8,2.3c-0.5,0.5-1.3,0.8-2.3,0.8C40.2,52.2,39.4,51.6,38.8,50.8z"/> 45 | <path id="XMLID_3028_" style="fill:#FFFFFF;" d="M45.3,43.8l6.7,0v1.9l-4.4,0V47l4,0l0,1.8l-4,0l0,1.3l4.5,0l0,2l-6.7,0 46 | L45.3,43.8z"/> 47 | <path id="XMLID_3026_" style="fill:#FFFFFF;" d="M55,45.8l-2.5,0l0-2l7.3,0l0,2l-2.5,0l0,6.3l-2.3,0L55,45.8z"/> 48 | <path id="XMLID_3022_" style="fill:#FFFFFF;" d="M39,54l4.3,0c1,0,1.8,0.3,2.3,0.7c0.3,0.3,0.5,0.8,0.5,1.4v0 49 | c0,1-0.5,1.5-1.3,1.9c1,0.3,1.6,0.9,1.6,2v0c0,1.4-1.2,2.3-3.1,2.3l-4.3,0L39,54z M43.8,56.6c0-0.5-0.4-0.7-1-0.7l-1.5,0l0,1.5 50 | l1.4,0C43.4,57.3,43.8,57.1,43.8,56.6L43.8,56.6z M43,59l-1.8,0l0,1.5H43c0.7,0,1.1-0.3,1.1-0.8v0C44.1,59.2,43.7,59,43,59z"/> 51 | <path id="XMLID_3019_" style="fill:#FFFFFF;" d="M46.8,54l3.9,0c1.3,0,2.1,0.3,2.7,0.9c0.5,0.5,0.7,1.1,0.7,1.9v0 52 | c0,1.3-0.7,2.1-1.7,2.6l2,2.9l-2.6,0l-1.7-2.5h-1l0,2.5l-2.3,0L46.8,54z M50.6,58c0.8,0,1.2-0.4,1.2-1v0c0-0.7-0.5-1-1.2-1 53 | l-1.5,0v2H50.6z"/> 54 | <path id="XMLID_3016_" style="fill:#FFFFFF;" d="M56.8,54l2.2,0l3.5,8.4l-2.5,0l-0.6-1.5l-3.2,0l-0.6,1.5l-2.4,0L56.8,54z 55 | M58.8,59l-0.9-2.3L57,59L58.8,59z"/> 56 | <path id="XMLID_3014_" style="fill:#FFFFFF;" d="M62.8,54l2.3,0l0,8.3l-2.3,0L62.8,54z"/> 57 | <path id="XMLID_3012_" style="fill:#FFFFFF;" d="M65.7,54l2.1,0l3.4,4.4l0-4.4l2.3,0l0,8.3l-2,0L68,57.8l0,4.6l-2.3,0L65.7,54z" 58 | /> 59 | <path id="XMLID_3010_" style="fill:#FFFFFF;" d="M73.7,61.1l1.3-1.5c0.8,0.7,1.7,1,2.7,1c0.6,0,1-0.2,1-0.6v0 60 | c0-0.4-0.3-0.5-1.4-0.8c-1.8-0.4-3.1-0.9-3.1-2.6v0c0-1.5,1.2-2.7,3.2-2.7c1.4,0,2.5,0.4,3.4,1.1l-1.2,1.6 61 | c-0.8-0.5-1.6-0.8-2.3-0.8c-0.6,0-0.8,0.2-0.8,0.5v0c0,0.4,0.3,0.5,1.4,0.8c1.9,0.4,3.1,1,3.1,2.6v0c0,1.7-1.3,2.7-3.4,2.7 62 | C76.1,62.5,74.7,62,73.7,61.1z"/> 63 | </g> 64 | </g> 65 | </g> 66 | </svg> 67 | -------------------------------------------------------------------------------- /docs/resources/additional-resources.md: -------------------------------------------------------------------------------- 1 | # Additional resources 2 | 3 | ### Understanding phone numbers 4 | 5 | - [whitepages.fr/phonesystem](http://whitepages.fr/phonesystem/) 6 | - [Formatting-International-Phone-Numbers](https://support.twilio.com/hc/en-us/articles/223183008-Formatting-International-Phone-Numbers) 7 | - [National_conventions_for_writing_telephone_numbers](https://en.wikipedia.org/wiki/National_conventions_for_writing_telephone_numbers) 8 | 9 | ### Open data 10 | 11 | - [api.ovh.com/console/#/telephony](https://api.ovh.com/console/#/telephony) 12 | - [countrycode.org](https://countrycode.org/) 13 | - [countryareacode.net](http://www.countryareacode.net/en/) 14 | - [directory.didww.com/area-prefixes](http://directory.didww.com/area-prefixes) 15 | - [numinfo.net](http://www.numinfo.net/) 16 | - [gist.github.com/Goles/3196253](https://gist.github.com/Goles/3196253) 17 | 18 | ## Footprinting 19 | 20 | !!! info 21 | Both free and premium resources are included. Be careful, the listing of a data source here does not mean it has been verified or is used in the tool. Data might be false. Use it as an OSINT framework. 22 | 23 | ### Reputation / fraud 24 | 25 | - scamcallfighters.com 26 | - signal-arnaques.com 27 | - whosenumber.info 28 | - findwhocallsme.com 29 | - yellowpages.ca 30 | - phonenumbers.ie 31 | - who-calledme.com 32 | - usphonesearch.net 33 | - whocalled.us 34 | - quinumero.info 35 | 36 | ### Disposable numbers 37 | 38 | - receive-sms-online.com 39 | - receive-sms-now.com 40 | - hs3x.com 41 | - twilio.com 42 | - freesmsverification.com 43 | - freeonlinephone.org 44 | - sms-receive.net 45 | - smsreceivefree.com 46 | - receive-a-sms.com 47 | - receivefreesms.com 48 | - freephonenum.com 49 | - receive-smss.com 50 | - receivetxt.com 51 | - temp-mails.com 52 | - receive-sms.com 53 | - receivesmsonline.net 54 | - receivefreesms.com 55 | - sms-receive.net 56 | - pinger.com (=> textnow.com) 57 | - receive-a-sms.com 58 | - k7.net 59 | - kall8.com 60 | - faxaway.com 61 | - receivesmsonline.com 62 | - receive-sms-online.info 63 | - sellaite.com 64 | - getfreesmsnumber.com 65 | - smsreceiving.com 66 | - smstibo.com 67 | - catchsms.com 68 | - freesmscode.com 69 | - smsreceiveonline.com 70 | - smslisten.com 71 | - sms.sellaite.com 72 | - smslive.co 73 | 74 | ### Individuals 75 | 76 | - Facebook 77 | - Twitter 78 | - Instagram 79 | - Linkedin 80 | - True People 81 | - Fast People 82 | - Background Check 83 | - Pipl 84 | - Spytox 85 | - Makelia 86 | - IvyCall 87 | - PhoneSearch 88 | - 411 89 | - USPhone 90 | - WP Plus 91 | - Thats Them 92 | - True Caller 93 | - Sync.me 94 | - WhoCallsMe 95 | - ZabaSearch 96 | - DexKnows 97 | - WeLeakInfo 98 | - OK Caller 99 | - SearchBug 100 | - numinfo.net 101 | 102 | ### Google dork examples 103 | 104 | ``` 105 | insubject:"+XXXXXXXXX" OR insubject:"+XXXXX" OR insubject:"XXXXX XXX XXX" 106 | insubject:"XXXXXXXXX" OR intitle:"XXXXXXXXX" 107 | intext:"XXXXXXXXX" AND (ext:doc OR ext:docx OR ext:odt OR ext:pdf OR ext:rtf OR ext:sxw OR ext:psw OR ext:ppt OR ext:pptx OR ext:pps OR ext:csv OR ext:txt OR ext:html) 108 | site:"hs3x.com" "+XXXXXXXXX" 109 | site:signal-arnaques.com intext:"XXXXXXXXX" intitle:" | Phone Fraud" 110 | ``` 111 | -------------------------------------------------------------------------------- /docs/resources/formatting.md: -------------------------------------------------------------------------------- 1 | # Formatting phone numbers 2 | 3 | ## Basics 4 | 5 | The tool only accepts E164 and International formats as input. 6 | 7 | - E164: +3396360XXXX 8 | - International: +33 9 63 60 XX XX 9 | - National: 09 63 60 XX XX 10 | - RFC3966: tel:+33-9-63-60-XX-XX 11 | - Out-of-country format from US: 011 33 9 63 60 XX XX 12 | 13 | E.164 formatting for phone numbers entails the following: 14 | 15 | - A + (plus) sign 16 | - International Country Calling code 17 | - Local Area code 18 | - Local Phone number 19 | 20 | For example, here’s a US-based number in standard local formatting: (415) 555-2671 21 | 22 |  23 | 24 | Here’s the same phone number in E.164 formatting: +14155552671 25 | 26 |  27 | 28 | In the UK, and many other countries internationally, local dialing may require the addition of a '0' in front of the subscriber number. With E.164 formatting, this '0' must usually be removed. 29 | 30 | For example, here’s a UK-based number in standard local formatting: 020 7183 8750 31 | 32 | Here’s the same phone number in E.164 formatting: +442071838750 33 | 34 | 35 | ## Custom formatting 36 | 37 | Sometimes the phone number has footprints but is used with a different formatting. This is a problem because for example if we search for "+15417543010", we'll not find web pages that write it that way : "(541) 754–3010". You should use a format that someone of the number's country would usually use to share the phone number online. 38 | 39 | For example, French people usually write numbers that way online : `06.20.30.40.50` or `06 20 30 40 50`. 40 | 41 | For US-based numbers, the most common format is : `543-456-1234`. 42 | 43 | ### Examples 44 | 45 | Here are some examples of custom formatting for US-based phone numbers : 46 | 47 | - `+1 618-555-xxxx` 48 | - `(+1)618-555-xxxx` 49 | - `+1/618-555-xxxx` 50 | - `(618) 555xxxx` 51 | - `(618) 555-xxxx` 52 | - `(618) 555.xxxx` 53 | - `(618)555xxxx` 54 | - `(618)555-xxxx` 55 | - `(618)555.xxxx` 56 | 57 | For European countries (France as example) : 58 | 59 | - `+3301 86 48 xx xx` 60 | - `+33018648xxxx` 61 | - `+33018 648 xxx x` 62 | - `(0033)018648xxxx` 63 | - `(+33)018 648 xxx x` 64 | - `+33/018648xxxx` 65 | - `(0033)018 648 xxx x` 66 | - `+33018-648-xxx-x` 67 | - `(+33)018648xxxx` 68 | - `(+33)01 86 48 xx xx` 69 | - `+33/018-648-xxx-x` 70 | - `+33/01-86-48-xx-xx` 71 | - `+3301-86-48-xx-xx` 72 | - `(0033)01 86 48 xx xx` 73 | - `+33/01 86 48 xx xx` 74 | - `(+33)018-648-xxx-x` 75 | - `(+33)01-86-48-xx-xx` 76 | - `(0033)01-86-48-xx-xx` 77 | - `(0033)018-648-xxx-x` 78 | - `+33/018 648 xxx x` 79 | -------------------------------------------------------------------------------- /examples/plugin/README.md: -------------------------------------------------------------------------------- 1 | # Example plugin 2 | 3 | This is an example scanner plugin. 4 | 5 | ## Build 6 | 7 | ```shell 8 | $ go build -buildmode=plugin ./customscanner.go 9 | ``` 10 | 11 | Depending on your OS, it will create a plugin file (e.g. `customscanner.so` for linux). 12 | 13 | ## Usage 14 | 15 | You can now use this plugin with phoneinfoga. 16 | 17 | ```shell 18 | $ phoneinfoga scan -n <number> --plugin ./customscanner.so 19 | 20 | Running scan for phone number <number>... 21 | 22 | Results for customscanner 23 | Valid: true 24 | Info: This number is known for scams! 25 | 26 | ... 27 | ``` 28 | 29 | The `--plugin` flag can be used multiple times to use several plugins at once. 30 | -------------------------------------------------------------------------------- /examples/plugin/customscanner.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/sundowndev/phoneinfoga/v2/lib/number" 5 | "github.com/sundowndev/phoneinfoga/v2/lib/remote" 6 | ) 7 | 8 | type customScanner struct{} 9 | 10 | type customScannerResponse struct { 11 | Valid bool `json:"valid" console:"Valid"` 12 | Info string `json:"info" console:"Info"` 13 | Hidden string `json:"-" console:"-"` 14 | } 15 | 16 | // Name returns the unique name this scanner. 17 | func (s *customScanner) Name() string { 18 | return "customscanner" 19 | } 20 | 21 | // Description returns a short description for this scanner. 22 | func (s *customScanner) Description() string { 23 | return "This is a dummy scanner" 24 | } 25 | 26 | // DryRun returns an error indicating whether 27 | // this scanner can be used with the given number. 28 | // This can be useful to check for authentication or 29 | // country code support for example, and avoid running 30 | // the scanner when it just can't work. 31 | func (s *customScanner) DryRun(n number.Number, opts remote.ScannerOptions) error { 32 | return nil 33 | } 34 | 35 | // Run does the actual scan of the phone number. 36 | // Note this function will be executed in a goroutine. 37 | func (s *customScanner) Run(n number.Number, opts remote.ScannerOptions) (interface{}, error) { 38 | data := customScannerResponse{ 39 | Valid: true, 40 | Info: "This number is known for scams!", 41 | Hidden: "This will not appear in the output", 42 | } 43 | return data, nil 44 | } 45 | 46 | func init() { 47 | remote.RegisterPlugin(&customScanner{}) 48 | } 49 | -------------------------------------------------------------------------------- /examples/plugin/customscanner_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "github.com/sundowndev/phoneinfoga/v2/lib/number" 6 | "github.com/sundowndev/phoneinfoga/v2/lib/remote" 7 | "testing" 8 | ) 9 | 10 | func TestCustomScanner_Metadata(t *testing.T) { 11 | scanner := &customScanner{} 12 | assert.Equal(t, "customscanner", scanner.Name()) 13 | assert.NotEmpty(t, scanner.Description()) 14 | } 15 | 16 | func TestCustomScanner(t *testing.T) { 17 | testcases := []struct { 18 | name string 19 | number *number.Number 20 | expected customScannerResponse 21 | wantError string 22 | }{ 23 | { 24 | name: "test successful scan", 25 | number: func() *number.Number { 26 | n, _ := number.NewNumber("15556661212") 27 | return n 28 | }(), 29 | expected: customScannerResponse{ 30 | Valid: true, 31 | Info: "This number is known for scams!", 32 | Hidden: "This will not appear in the output", 33 | }, 34 | }, 35 | } 36 | 37 | for _, tt := range testcases { 38 | t.Run(tt.name, func(t *testing.T) { 39 | scanner := &customScanner{} 40 | 41 | if scanner.DryRun(*tt.number, remote.ScannerOptions{}) != nil { 42 | t.Fatal("DryRun() should return nil") 43 | } 44 | 45 | got, err := scanner.Run(*tt.number, remote.ScannerOptions{}) 46 | if tt.wantError != "" { 47 | assert.EqualError(t, err, tt.wantError) 48 | } else { 49 | assert.NoError(t, err) 50 | } 51 | assert.Equal(t, tt.expected, got) 52 | }) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/sundowndev/phoneinfoga/v2 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/fatih/color v1.13.0 7 | github.com/gin-gonic/gin v1.9.1 8 | github.com/joho/godotenv v1.4.0 9 | github.com/nyaruka/phonenumbers v1.1.0 10 | github.com/onlinecity/go-phone-iso3166 v0.0.1 11 | github.com/sirupsen/logrus v1.8.1 12 | github.com/spf13/cobra v1.1.3 13 | github.com/stretchr/testify v1.8.3 14 | github.com/sundowndev/dorkgen v1.3.1 15 | github.com/swaggo/swag v1.16.1 16 | google.golang.org/api v0.92.0 17 | gopkg.in/h2non/gock.v1 v1.0.16 18 | ) 19 | 20 | require ( 21 | cloud.google.com/go/compute v1.7.0 // indirect 22 | github.com/KyleBanks/depth v1.2.1 // indirect 23 | github.com/PuerkitoBio/purell v1.1.1 // indirect 24 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect 25 | github.com/bytedance/sonic v1.9.1 // indirect 26 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect 27 | github.com/davecgh/go-spew v1.1.1 // indirect 28 | github.com/gabriel-vasile/mimetype v1.4.2 // indirect 29 | github.com/gin-contrib/sse v0.1.0 // indirect 30 | github.com/go-openapi/jsonpointer v0.19.5 // indirect 31 | github.com/go-openapi/jsonreference v0.19.6 // indirect 32 | github.com/go-openapi/spec v0.20.4 // indirect 33 | github.com/go-openapi/swag v0.19.15 // indirect 34 | github.com/go-playground/locales v0.14.1 // indirect 35 | github.com/go-playground/universal-translator v0.18.1 // indirect 36 | github.com/go-playground/validator/v10 v10.14.0 // indirect 37 | github.com/goccy/go-json v0.10.2 // indirect 38 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect 39 | github.com/golang/protobuf v1.5.2 // indirect 40 | github.com/google/uuid v1.3.0 // indirect 41 | github.com/googleapis/enterprise-certificate-proxy v0.1.0 // indirect 42 | github.com/googleapis/gax-go/v2 v2.4.0 // indirect 43 | github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect 44 | github.com/hashicorp/go-immutable-radix v1.1.0 // indirect 45 | github.com/hashicorp/golang-lru v0.5.4 // indirect 46 | github.com/inconshreveable/mousetrap v1.0.0 // indirect 47 | github.com/josharian/intern v1.0.0 // indirect 48 | github.com/json-iterator/go v1.1.12 // indirect 49 | github.com/klauspost/cpuid/v2 v2.2.4 // indirect 50 | github.com/leodido/go-urn v1.2.4 // indirect 51 | github.com/mailru/easyjson v0.7.6 // indirect 52 | github.com/mattn/go-colorable v0.1.9 // indirect 53 | github.com/mattn/go-isatty v0.0.19 // indirect 54 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 55 | github.com/modern-go/reflect2 v1.0.2 // indirect 56 | github.com/pelletier/go-toml/v2 v2.0.8 // indirect 57 | github.com/pmezard/go-difflib v1.0.0 // indirect 58 | github.com/spf13/pflag v1.0.5 // indirect 59 | github.com/stretchr/objx v0.5.0 // indirect 60 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 61 | github.com/ugorji/go/codec v1.2.11 // indirect 62 | go.opencensus.io v0.23.0 // indirect 63 | golang.org/x/arch v0.3.0 // indirect 64 | golang.org/x/crypto v0.9.0 // indirect 65 | golang.org/x/net v0.10.0 // indirect 66 | golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2 // indirect 67 | golang.org/x/sys v0.8.0 // indirect 68 | golang.org/x/text v0.9.0 // indirect 69 | golang.org/x/tools v0.7.0 // indirect 70 | google.golang.org/appengine v1.6.7 // indirect 71 | google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f // indirect 72 | google.golang.org/grpc v1.47.0 // indirect 73 | google.golang.org/protobuf v1.30.0 // indirect 74 | gopkg.in/yaml.v2 v2.4.0 // indirect 75 | gopkg.in/yaml.v3 v3.0.1 // indirect 76 | ) 77 | -------------------------------------------------------------------------------- /lib/filter/filter.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | type Filter interface { 4 | Match(string) bool 5 | } 6 | 7 | type Engine struct { 8 | rules []string 9 | } 10 | 11 | func NewEngine() *Engine { 12 | return &Engine{} 13 | } 14 | 15 | func (e *Engine) AddRule(r ...string) { 16 | e.rules = append(e.rules, r...) 17 | } 18 | 19 | func (e *Engine) Match(r string) bool { 20 | for _, rule := range e.rules { 21 | if rule == r { 22 | return true 23 | } 24 | } 25 | return false 26 | } 27 | -------------------------------------------------------------------------------- /lib/filter/filter_test.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestFilterEngine(t *testing.T) { 9 | testcases := []struct { 10 | name string 11 | rules []string 12 | expected map[string]bool 13 | }{ 14 | { 15 | name: "test googlesearch is ignored", 16 | rules: []string{"googlesearch"}, 17 | expected: map[string]bool{ 18 | "googlesearch": true, 19 | "numverify": false, 20 | }, 21 | }, 22 | { 23 | name: "test none is ignored", 24 | rules: []string{}, 25 | expected: map[string]bool{ 26 | "googlesearch": false, 27 | "numverify": false, 28 | }, 29 | }, 30 | } 31 | 32 | for _, tt := range testcases { 33 | t.Run(tt.name, func(t *testing.T) { 34 | e := NewEngine() 35 | e.AddRule(tt.rules...) 36 | for r, isIgnored := range tt.expected { 37 | assert.Equal(t, isIgnored, e.Match(r)) 38 | } 39 | }) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/number/number.go: -------------------------------------------------------------------------------- 1 | package number 2 | 3 | import ( 4 | "github.com/nyaruka/phonenumbers" 5 | ) 6 | 7 | // Number is a phone number 8 | type Number struct { 9 | Valid bool 10 | RawLocal string 11 | Local string 12 | E164 string 13 | International string 14 | CountryCode int32 15 | Country string 16 | Carrier string 17 | } 18 | 19 | func NewNumber(number string) (res *Number, err error) { 20 | n := "+" + FormatNumber(number) 21 | country := ParseCountryCode(n) 22 | 23 | num, err := phonenumbers.Parse(n, country) 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | res = &Number{ 29 | Valid: phonenumbers.IsValidNumber(num), 30 | RawLocal: FormatNumber(phonenumbers.Format(num, phonenumbers.NATIONAL)), 31 | Local: phonenumbers.Format(num, phonenumbers.NATIONAL), 32 | E164: phonenumbers.Format(num, phonenumbers.E164), 33 | International: FormatNumber(phonenumbers.Format(num, phonenumbers.E164)), 34 | CountryCode: num.GetCountryCode(), 35 | Country: country, 36 | Carrier: num.GetPreferredDomesticCarrierCode(), 37 | } 38 | 39 | return res, nil 40 | } 41 | -------------------------------------------------------------------------------- /lib/number/number_test.go: -------------------------------------------------------------------------------- 1 | package number 2 | 3 | import ( 4 | "errors" 5 | "github.com/stretchr/testify/assert" 6 | "testing" 7 | ) 8 | 9 | func TestNumber(t *testing.T) { 10 | cases := []struct { 11 | name string 12 | input string 13 | expected *Number 14 | wantErr error 15 | }{ 16 | { 17 | name: "should succeed to parse number", 18 | input: "33678342311", 19 | expected: &Number{ 20 | Valid: true, 21 | RawLocal: "0678342311", 22 | Local: "06 78 34 23 11", 23 | E164: "+33678342311", 24 | International: "33678342311", 25 | CountryCode: 33, 26 | Country: "FR", 27 | Carrier: "", 28 | }, 29 | }, 30 | { 31 | name: "should succeed to parse number", 32 | input: "15552221212", 33 | expected: &Number{ 34 | Valid: false, 35 | RawLocal: "5552221212", 36 | Local: "(555) 222-1212", 37 | E164: "+15552221212", 38 | International: "15552221212", 39 | CountryCode: 1, 40 | Country: "", 41 | Carrier: "", 42 | }, 43 | }, 44 | 45 | { 46 | name: "should fail to parse number", 47 | input: "wrong", 48 | expected: nil, 49 | wantErr: errors.New("the phone number supplied is not a number"), 50 | }, 51 | } 52 | 53 | for _, tt := range cases { 54 | t.Run(tt.name, func(t *testing.T) { 55 | num, err := NewNumber(tt.input) 56 | assert.Equal(t, tt.wantErr, err) 57 | assert.Equal(t, tt.expected, num) 58 | }) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /lib/number/utils.go: -------------------------------------------------------------------------------- 1 | package number 2 | 3 | import ( 4 | "regexp" 5 | "strconv" 6 | 7 | phoneiso3166 "github.com/onlinecity/go-phone-iso3166" 8 | ) 9 | 10 | // FormatNumber formats a phone number to remove 11 | // unnecessary chars and avoid dealing with unwanted input. 12 | func FormatNumber(n string) string { 13 | re := regexp.MustCompile(`[_\W]+`) 14 | number := re.ReplaceAllString(n, "") 15 | 16 | return number 17 | } 18 | 19 | // ParseCountryCode parses a phone number and returns ISO country code. 20 | // This is required in order to use the phonenumbers library. 21 | func ParseCountryCode(n string) string { 22 | var number uint64 23 | number, _ = strconv.ParseUint(FormatNumber(n), 10, 64) 24 | 25 | return phoneiso3166.E164.Lookup(number) 26 | } 27 | 28 | // IsValid indicate if a phone number has a valid format. 29 | func IsValid(number string) bool { 30 | number = FormatNumber(number) 31 | 32 | re := regexp.MustCompile("^[0-9]+quot;) 33 | 34 | return len(re.FindString(number)) != 0 35 | } 36 | -------------------------------------------------------------------------------- /lib/number/utils_test.go: -------------------------------------------------------------------------------- 1 | package number 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestUtils(t *testing.T) { 9 | t.Run("FormatNumber", func(t *testing.T) { 10 | t.Run("should format number correctly", func(t *testing.T) { 11 | result := FormatNumber("+1 555-444-2222") 12 | 13 | assert.Equal(t, result, "15554442222", "they should be equal") 14 | }) 15 | 16 | t.Run("should format number correctly", func(t *testing.T) { 17 | result := FormatNumber("+1 (315) 284-1580") 18 | 19 | assert.Equal(t, result, "13152841580", "they should be equal") 20 | }) 21 | }) 22 | 23 | t.Run("ParseCountryCode", func(t *testing.T) { 24 | t.Run("should parse country code correctly", func(t *testing.T) { 25 | result := ParseCountryCode("+33 679368229") 26 | 27 | assert.Equal(t, result, "FR", "they should be equal") 28 | }) 29 | 30 | t.Run("should parse country code correctly", func(t *testing.T) { 31 | result := ParseCountryCode("+1 315-284-1580") 32 | 33 | assert.Equal(t, result, "US", "they should be equal") 34 | }) 35 | 36 | t.Run("should parse country code correctly", func(t *testing.T) { 37 | result := ParseCountryCode("4566118311") 38 | 39 | assert.Equal(t, result, "DK", "they should be equal") 40 | }) 41 | }) 42 | 43 | t.Run("IsValid", func(t *testing.T) { 44 | t.Run("should validate phone number", func(t *testing.T) { 45 | result := IsValid("+1 315-284-1580") 46 | 47 | assert.Equal(t, result, true, "they should be equal") 48 | }) 49 | 50 | t.Run("should validate phone number", func(t *testing.T) { 51 | result := IsValid("P+1 315-284-1580A") 52 | 53 | assert.Equal(t, result, false, "they should be equal") 54 | }) 55 | }) 56 | } 57 | -------------------------------------------------------------------------------- /lib/output/console.go: -------------------------------------------------------------------------------- 1 | package output 2 | 3 | import ( 4 | "fmt" 5 | "github.com/fatih/color" 6 | "github.com/sirupsen/logrus" 7 | "io" 8 | "reflect" 9 | "sort" 10 | "strings" 11 | ) 12 | 13 | type ConsoleOutput struct { 14 | w io.Writer 15 | } 16 | 17 | func NewConsoleOutput(w io.Writer) *ConsoleOutput { 18 | return &ConsoleOutput{w: w} 19 | } 20 | 21 | func (o *ConsoleOutput) Write(result map[string]interface{}, errs map[string]error) error { 22 | succeeded := 0 23 | for _, name := range getSortedResultKeys(result) { 24 | res := result[name] 25 | if res == nil { 26 | logrus.WithField("name", name).Debug("Scanner returned result <nil>") 27 | continue 28 | } 29 | _, _ = fmt.Fprintf(o.w, color.WhiteString("Results for %s\n"), name) 30 | o.displayResult(res, "") 31 | _, _ = fmt.Fprintf(o.w, "\n") 32 | succeeded++ 33 | } 34 | 35 | if len(errs) > 0 { 36 | _, _ = fmt.Fprintln(o.w, "The following scanners returned errors:") 37 | for _, name := range getSortedErrorKeys(errs) { 38 | _, _ = fmt.Fprintf(o.w, "%s: %s\n", name, errs[name]) 39 | } 40 | _, _ = fmt.Fprintf(o.w, "\n") 41 | } 42 | 43 | _, _ = fmt.Fprintf(o.w, "%d scanner(s) succeeded\n", succeeded) 44 | 45 | return nil 46 | } 47 | 48 | func (o *ConsoleOutput) displayResult(val interface{}, prefix string) { 49 | reflectType := reflect.TypeOf(val) 50 | reflectValue := reflect.ValueOf(val) 51 | 52 | if reflectValue.Kind() == reflect.Slice { 53 | for i := 0; i < reflectValue.Len(); i++ { 54 | item := reflectValue.Index(i) 55 | if item.Kind() == reflect.Ptr { 56 | item = reflectValue.Index(i).Elem() 57 | } 58 | o.displayResult(item.Interface(), prefix) 59 | 60 | // If it's the latest element, add a newline 61 | if i < reflectValue.Len()-1 { 62 | _, _ = fmt.Fprintf(o.w, "\n") 63 | } 64 | } 65 | return 66 | } 67 | 68 | for i := 0; i < reflectType.NumField(); i++ { 69 | valueValue := reflectValue.Field(i).Interface() 70 | 71 | field, ok := reflectType.Field(i).Tag.Lookup("console") 72 | if !ok || field == "-" { 73 | continue 74 | } 75 | 76 | if strings.Contains(field, "omitempty") && reflectValue.Field(i).IsZero() { 77 | continue 78 | } 79 | fieldTitle := strings.Split(field, ",")[0] 80 | 81 | switch reflectValue.Field(i).Kind() { 82 | case reflect.String: 83 | _, _ = fmt.Fprintf(o.w, "%s%s: ", prefix, fieldTitle) 84 | _, _ = fmt.Fprintf(o.w, color.YellowString("%s\n"), valueValue) 85 | case reflect.Bool: 86 | _, _ = fmt.Fprintf(o.w, "%s%s: ", prefix, fieldTitle) 87 | _, _ = fmt.Fprintf(o.w, color.YellowString("%v\n"), valueValue) 88 | case reflect.Int: 89 | _, _ = fmt.Fprintf(o.w, "%s%s: ", prefix, fieldTitle) 90 | _, _ = fmt.Fprintf(o.w, color.YellowString("%d\n"), valueValue) 91 | case reflect.Struct: 92 | _, _ = fmt.Fprintf(o.w, "%s%s:\n", prefix, fieldTitle) 93 | o.displayResult(valueValue, prefix+"\t") 94 | case reflect.Slice: 95 | _, _ = fmt.Fprintf(o.w, color.WhiteString("%s:\n"), fieldTitle) 96 | o.displayResult(valueValue, prefix+"\t") 97 | } 98 | } 99 | } 100 | 101 | func getSortedResultKeys(m map[string]interface{}) []string { 102 | keys := make([]string, 0, len(m)) 103 | for k := range m { 104 | keys = append(keys, k) 105 | } 106 | sort.Strings(keys) 107 | return keys 108 | } 109 | 110 | func getSortedErrorKeys(m map[string]error) []string { 111 | keys := make([]string, 0, len(m)) 112 | for k := range m { 113 | keys = append(keys, k) 114 | } 115 | sort.Strings(keys) 116 | return keys 117 | } 118 | -------------------------------------------------------------------------------- /lib/output/console_test.go: -------------------------------------------------------------------------------- 1 | package output 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "github.com/stretchr/testify/assert" 7 | "github.com/sundowndev/phoneinfoga/v2/lib/remote" 8 | "github.com/sundowndev/phoneinfoga/v2/test/goldenfile" 9 | "os" 10 | "testing" 11 | ) 12 | 13 | func TestConsoleOutput(t *testing.T) { 14 | type FakeScannerResponse struct { 15 | Format string `console:"Number Format"` 16 | } 17 | 18 | type FakeScannerResponseRecursive2 struct { 19 | Response struct { 20 | Format FakeScannerResponse `console:"Format"` 21 | } `console:"Response"` 22 | } 23 | 24 | testcases := []struct { 25 | name string 26 | dirName string 27 | result map[string]interface{} 28 | errs map[string]error 29 | wantErr error 30 | }{ 31 | { 32 | name: "should produce empty output", 33 | dirName: "testdata/console_empty.txt", 34 | result: map[string]interface{}{}, 35 | errs: map[string]error{}, 36 | }, 37 | { 38 | name: "should produce valid output", 39 | dirName: "testdata/console_valid.txt", 40 | result: map[string]interface{}{ 41 | "numverify": remote.NumverifyScannerResponse{ 42 | Valid: true, 43 | Number: "test", 44 | LocalFormat: "test", 45 | InternationalFormat: "test", 46 | CountryPrefix: "test", 47 | CountryCode: "test", 48 | CountryName: "test", 49 | Location: "test", 50 | Carrier: "test", 51 | LineType: "test", 52 | }, 53 | }, 54 | errs: map[string]error{}, 55 | }, 56 | { 57 | name: "should produce valid output with errors", 58 | dirName: "testdata/console_valid_with_errors.txt", 59 | result: map[string]interface{}{ 60 | "testscanner": nil, 61 | "numverify": remote.NumverifyScannerResponse{ 62 | Valid: true, 63 | Number: "test", 64 | LocalFormat: "test", 65 | InternationalFormat: "test", 66 | CountryPrefix: "test", 67 | CountryCode: "test", 68 | CountryName: "test", 69 | Location: "test", 70 | Carrier: "test", 71 | LineType: "test", 72 | }, 73 | "testscanner2": FakeScannerResponse{ 74 | Format: "test", 75 | }, 76 | }, 77 | errs: map[string]error{ 78 | "googlesearch": errors.New("dummy error"), 79 | "fakescanner": errors.New("dummy error 2"), 80 | }, 81 | }, 82 | { 83 | name: "should follow recursive paths", 84 | dirName: "testdata/console_valid_recursive.txt", 85 | result: map[string]interface{}{ 86 | "testscanner": remote.GoogleSearchResponse{ 87 | SocialMedia: []*remote.GoogleSearchDork{ 88 | { 89 | URL: "http://example.com?q=111-555-1212", 90 | Number: "111-555-1212", 91 | Dork: "intext:\"111-555-1212\"", 92 | }, 93 | { 94 | URL: "http://example.com?q=222-666-2323", 95 | Number: "222-666-2323", 96 | Dork: "intext:\"222-666-2323\"", 97 | }, 98 | }, 99 | }, 100 | "testscanner2": FakeScannerResponseRecursive2{ 101 | Response: struct { 102 | Format FakeScannerResponse `console:"Format"` 103 | }{Format: FakeScannerResponse{Format: "test"}}, 104 | }, 105 | }, 106 | errs: map[string]error{}, 107 | }, 108 | } 109 | 110 | for _, tt := range testcases { 111 | t.Run(tt.name, func(t *testing.T) { 112 | shouldUpdate := tt.dirName == *goldenfile.Update 113 | 114 | expected, err := os.ReadFile(tt.dirName) 115 | if err != nil && !shouldUpdate { 116 | t.Fatal(err) 117 | } 118 | 119 | got := new(bytes.Buffer) 120 | err = GetOutput(Console, got).Write(tt.result, tt.errs) 121 | if tt.wantErr != nil { 122 | assert.EqualError(t, err, tt.wantErr.Error()) 123 | } else { 124 | assert.Nil(t, err) 125 | } 126 | 127 | if shouldUpdate { 128 | err = os.WriteFile(tt.dirName, got.Bytes(), 0644) 129 | if err != nil { 130 | t.Fatal(err) 131 | } 132 | expected, err = os.ReadFile(tt.dirName) 133 | if err != nil { 134 | t.Fatal(err) 135 | } 136 | } 137 | 138 | assert.Equal(t, string(expected), got.String()) 139 | }) 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /lib/output/output.go: -------------------------------------------------------------------------------- 1 | package output 2 | 3 | import ( 4 | "io" 5 | ) 6 | 7 | type Output interface { 8 | Write(map[string]interface{}, map[string]error) error 9 | } 10 | 11 | type OutputKey int 12 | 13 | const ( 14 | Console OutputKey = iota + 1 15 | ) 16 | 17 | func GetOutput(o OutputKey, w io.Writer) Output { 18 | switch o { 19 | case Console: 20 | return NewConsoleOutput(w) 21 | } 22 | return nil 23 | } 24 | -------------------------------------------------------------------------------- /lib/output/testdata/console_empty.txt: -------------------------------------------------------------------------------- 1 | 0 scanner(s) succeeded 2 | -------------------------------------------------------------------------------- /lib/output/testdata/console_valid.txt: -------------------------------------------------------------------------------- 1 | Results for numverify 2 | Valid: true 3 | Number: test 4 | Local format: test 5 | International format: test 6 | Country prefix: test 7 | Country code: test 8 | Country name: test 9 | Location: test 10 | Carrier: test 11 | Line type: test 12 | 13 | 1 scanner(s) succeeded 14 | -------------------------------------------------------------------------------- /lib/output/testdata/console_valid_recursive.txt: -------------------------------------------------------------------------------- 1 | Results for testscanner 2 | Social media: 3 | URL: http://example.com?q=111-555-1212 4 | 5 | URL: http://example.com?q=222-666-2323 6 | 7 | Results for testscanner2 8 | Response: 9 | Format: 10 | Number Format: test 11 | 12 | 2 scanner(s) succeeded 13 | -------------------------------------------------------------------------------- /lib/output/testdata/console_valid_with_errors.txt: -------------------------------------------------------------------------------- 1 | Results for numverify 2 | Valid: true 3 | Number: test 4 | Local format: test 5 | International format: test 6 | Country prefix: test 7 | Country code: test 8 | Country name: test 9 | Location: test 10 | Carrier: test 11 | Line type: test 12 | 13 | Results for testscanner2 14 | Number Format: test 15 | 16 | The following scanners returned errors: 17 | fakescanner: dummy error 2 18 | googlesearch: dummy error 19 | 20 | 2 scanner(s) succeeded 21 | -------------------------------------------------------------------------------- /lib/remote/googlecse_scanner.go: -------------------------------------------------------------------------------- 1 | package remote 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "github.com/sundowndev/dorkgen" 8 | "github.com/sundowndev/dorkgen/googlesearch" 9 | "github.com/sundowndev/phoneinfoga/v2/lib/number" 10 | "google.golang.org/api/customsearch/v1" 11 | "google.golang.org/api/googleapi" 12 | "google.golang.org/api/option" 13 | "net/http" 14 | "os" 15 | "strconv" 16 | ) 17 | 18 | const GoogleCSE = "googlecse" 19 | 20 | type googleCSEScanner struct { 21 | MaxResults int64 22 | httpClient *http.Client 23 | } 24 | 25 | type ResultItem struct { 26 | Title string `json:"title,omitempty" console:"Title,omitempty"` 27 | URL string `json:"url,omitempty" console:"URL,omitempty"` 28 | } 29 | 30 | type GoogleCSEScannerResponse struct { 31 | Homepage string `json:"homepage,omitempty" console:"Homepage,omitempty"` 32 | ResultCount int `json:"result_count" console:"Results shown"` 33 | TotalResultCount int `json:"total_result_count" console:"Total number of results"` 34 | TotalRequestCount int `json:"total_request_count" console:"Requests made"` 35 | Items []ResultItem `json:"items,omitempty" console:"Items,omitempty"` 36 | } 37 | 38 | func NewGoogleCSEScanner(HTTPclient *http.Client) Scanner { 39 | // CSE limits you to 10 pages of results with max 10 results per page 40 | // We only fetch the first page of results by default for each request 41 | maxResults := 10 42 | if v := os.Getenv("GOOGLECSE_MAX_RESULTS"); v != "" { 43 | val, err := strconv.Atoi(v) 44 | if err == nil { 45 | if val > 100 { 46 | val = 100 47 | } 48 | maxResults = val 49 | } 50 | } 51 | 52 | return &googleCSEScanner{ 53 | MaxResults: int64(maxResults), 54 | httpClient: HTTPclient, 55 | } 56 | } 57 | 58 | func (s *googleCSEScanner) Name() string { 59 | return GoogleCSE 60 | } 61 | 62 | func (s *googleCSEScanner) Description() string { 63 | return "Googlecse searches for footprints of a given phone number on the web using Google Custom Search Engine." 64 | } 65 | 66 | func (s *googleCSEScanner) DryRun(_ number.Number, opts ScannerOptions) error { 67 | if opts.GetStringEnv("GOOGLECSE_CX") == "" || opts.GetStringEnv("GOOGLE_API_KEY") == "" { 68 | return errors.New("search engine ID and/or API key is not defined") 69 | } 70 | return nil 71 | } 72 | 73 | func (s *googleCSEScanner) Run(n number.Number, opts ScannerOptions) (interface{}, error) { 74 | var allItems []*customsearch.Result 75 | var dorks []*GoogleSearchDork 76 | var totalResultCount int 77 | var totalRequestCount int 78 | var cx = opts.GetStringEnv("GOOGLECSE_CX") 79 | var apikey = opts.GetStringEnv("GOOGLE_API_KEY") 80 | 81 | dorks = append(dorks, s.generateDorkQueries(n)...) 82 | 83 | customsearchService, err := customsearch.NewService( 84 | context.Background(), 85 | option.WithAPIKey(apikey), 86 | option.WithHTTPClient(s.httpClient), 87 | ) 88 | if err != nil { 89 | return nil, err 90 | } 91 | 92 | for _, req := range dorks { 93 | n, items, err := s.search(customsearchService, req.Dork, cx) 94 | if err != nil { 95 | if s.isRateLimit(err) { 96 | return nil, errors.New("rate limit exceeded, see https://developers.google.com/custom-search/v1/overview#pricing") 97 | } 98 | return nil, err 99 | } 100 | allItems = append(allItems, items...) 101 | totalResultCount += n 102 | totalRequestCount++ 103 | } 104 | 105 | var data GoogleCSEScannerResponse 106 | for _, item := range allItems { 107 | data.Items = append(data.Items, ResultItem{ 108 | Title: item.Title, 109 | URL: item.Link, 110 | }) 111 | } 112 | data.Homepage = fmt.Sprintf("https://cse.google.com/cse?cx=%s", cx) 113 | data.ResultCount = len(allItems) 114 | data.TotalResultCount = totalResultCount 115 | data.TotalRequestCount = totalRequestCount 116 | 117 | return data, nil 118 | } 119 | 120 | func (s *googleCSEScanner) search(service *customsearch.Service, q string, cx string) (int, []*customsearch.Result, error) { 121 | var results []*customsearch.Result 122 | var totalResultCount int 123 | 124 | offset := int64(0) 125 | for offset < s.MaxResults { 126 | search := service.Cse.List() 127 | search.Cx(cx) 128 | search.Q(q) 129 | search.Start(offset) 130 | searchQuery, err := search.Do() 131 | if err != nil { 132 | return 0, nil, err 133 | } 134 | results = append(results, searchQuery.Items...) 135 | totalResultCount, err = strconv.Atoi(searchQuery.SearchInformation.TotalResults) 136 | if err != nil { 137 | return 0, nil, err 138 | } 139 | if totalResultCount <= int(s.MaxResults) { 140 | break 141 | } 142 | offset += int64(len(searchQuery.Items)) 143 | } 144 | 145 | return totalResultCount, results, nil 146 | } 147 | 148 | func (s *googleCSEScanner) isRateLimit(theError error) bool { 149 | if theError == nil { 150 | return false 151 | } 152 | var err *googleapi.Error 153 | if !errors.As(theError, &err) { 154 | return false 155 | } 156 | if theError.(*googleapi.Error).Code != 429 { 157 | return false 158 | } 159 | return true 160 | } 161 | 162 | func (s *googleCSEScanner) generateDorkQueries(number number.Number) (results []*GoogleSearchDork) { 163 | var dorks = []*googlesearch.GoogleSearch{ 164 | dorkgen.NewGoogleSearch(). 165 | InText(number.International). 166 | Or(). 167 | InText(number.E164). 168 | Or(). 169 | InText(number.RawLocal). 170 | Or(). 171 | InText(number.Local), 172 | dorkgen.NewGoogleSearch(). 173 | Group(dorkgen.NewGoogleSearch(). 174 | Ext("doc"). 175 | Or(). 176 | Ext("docx"). 177 | Or(). 178 | Ext("odt"). 179 | Or(). 180 | Ext("pdf"). 181 | Or(). 182 | Ext("rtf"). 183 | Or(). 184 | Ext("sxw"). 185 | Or(). 186 | Ext("psw"). 187 | Or(). 188 | Ext("ppt"). 189 | Or(). 190 | Ext("pptx"). 191 | Or(). 192 | Ext("pps"). 193 | Or(). 194 | Ext("csv"). 195 | Or(). 196 | Ext("txt"). 197 | Or(). 198 | Ext("xls")). 199 | InText(number.International). 200 | Or(). 201 | InText(number.E164). 202 | Or(). 203 | InText(number.RawLocal). 204 | Or(). 205 | InText(number.Local), 206 | } 207 | 208 | for _, dork := range dorks { 209 | results = append(results, &GoogleSearchDork{ 210 | Number: number.E164, 211 | Dork: dork.String(), 212 | URL: dork.URL(), 213 | }) 214 | } 215 | 216 | return results 217 | } 218 | -------------------------------------------------------------------------------- /lib/remote/init.go: -------------------------------------------------------------------------------- 1 | package remote 2 | 3 | import ( 4 | "github.com/sundowndev/phoneinfoga/v2/lib/remote/suppliers" 5 | ) 6 | 7 | func InitScanners(remote *Library) { 8 | numverifySupplier := suppliers.NewNumverifySupplier() 9 | ovhSupplier := suppliers.NewOVHSupplier() 10 | 11 | remote.AddScanner(NewLocalScanner()) 12 | remote.AddScanner(NewNumverifyScanner(numverifySupplier)) 13 | remote.AddScanner(NewGoogleSearchScanner()) 14 | remote.AddScanner(NewOVHScanner(ovhSupplier)) 15 | remote.AddScanner(NewGoogleCSEScanner(nil)) 16 | 17 | remote.LoadPlugins() 18 | } 19 | -------------------------------------------------------------------------------- /lib/remote/local_scanner.go: -------------------------------------------------------------------------------- 1 | package remote 2 | 3 | import ( 4 | "github.com/sundowndev/phoneinfoga/v2/lib/number" 5 | ) 6 | 7 | const Local = "local" 8 | 9 | type localScanner struct{} 10 | 11 | type LocalScannerResponse struct { 12 | RawLocal string `json:"raw_local,omitempty" console:"Raw local,omitempty"` 13 | Local string `json:"local,omitempty" console:"Local,omitempty"` 14 | E164 string `json:"e164,omitempty" console:"E164,omitempty"` 15 | International string `json:"international,omitempty" console:"International,omitempty"` 16 | CountryCode int32 `json:"country_code,omitempty" console:"Country code,omitempty"` 17 | Country string `json:"country,omitempty" console:"Country,omitempty"` 18 | Carrier string `json:"carrier,omitempty" console:"Carrier,omitempty"` 19 | } 20 | 21 | func NewLocalScanner() Scanner { 22 | return &localScanner{} 23 | } 24 | 25 | func (s *localScanner) Name() string { 26 | return Local 27 | } 28 | 29 | func (s *localScanner) Description() string { 30 | return "Gather offline info about a given phone number." 31 | } 32 | 33 | func (s *localScanner) DryRun(_ number.Number, _ ScannerOptions) error { 34 | return nil 35 | } 36 | 37 | func (s *localScanner) Run(n number.Number, _ ScannerOptions) (interface{}, error) { 38 | data := LocalScannerResponse{ 39 | RawLocal: n.RawLocal, 40 | Local: n.Local, 41 | E164: n.E164, 42 | International: n.International, 43 | CountryCode: n.CountryCode, 44 | Country: n.Country, 45 | Carrier: n.Carrier, 46 | } 47 | return data, nil 48 | } 49 | -------------------------------------------------------------------------------- /lib/remote/local_scanner_test.go: -------------------------------------------------------------------------------- 1 | package remote 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "github.com/sundowndev/phoneinfoga/v2/lib/filter" 6 | "github.com/sundowndev/phoneinfoga/v2/lib/number" 7 | "testing" 8 | ) 9 | 10 | func TestLocalScanner_Metadata(t *testing.T) { 11 | scanner := NewLocalScanner() 12 | assert.Equal(t, Local, scanner.Name()) 13 | assert.NotEmpty(t, scanner.Description()) 14 | } 15 | 16 | func TestLocalScanner(t *testing.T) { 17 | testcases := []struct { 18 | name string 19 | number *number.Number 20 | expected map[string]interface{} 21 | wantErrors map[string]error 22 | }{ 23 | { 24 | name: "successful scan", 25 | number: func() *number.Number { 26 | n, _ := number.NewNumber("15556661212") 27 | return n 28 | }(), 29 | expected: map[string]interface{}{ 30 | "local": LocalScannerResponse{ 31 | RawLocal: "5556661212", 32 | Local: "(555) 666-1212", 33 | E164: "+15556661212", 34 | International: "15556661212", 35 | CountryCode: 1, 36 | }, 37 | }, 38 | wantErrors: map[string]error{}, 39 | }, 40 | } 41 | 42 | for _, tt := range testcases { 43 | t.Run(tt.name, func(t *testing.T) { 44 | scanner := NewLocalScanner() 45 | remote := NewLibrary(filter.NewEngine()) 46 | remote.AddScanner(scanner) 47 | 48 | if scanner.DryRun(*tt.number, ScannerOptions{}) != nil { 49 | t.Fatal("DryRun() should return nil") 50 | } 51 | 52 | got, errs := remote.Scan(tt.number, ScannerOptions{}) 53 | if len(tt.wantErrors) > 0 { 54 | assert.Equal(t, tt.wantErrors, errs) 55 | } else { 56 | assert.Len(t, errs, 0) 57 | } 58 | assert.Equal(t, tt.expected, got) 59 | }) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /lib/remote/numverify_scanner.go: -------------------------------------------------------------------------------- 1 | package remote 2 | 3 | import ( 4 | "errors" 5 | "github.com/sundowndev/phoneinfoga/v2/lib/number" 6 | "github.com/sundowndev/phoneinfoga/v2/lib/remote/suppliers" 7 | ) 8 | 9 | const Numverify = "numverify" 10 | 11 | type numverifyScanner struct { 12 | client suppliers.NumverifySupplierInterface 13 | } 14 | 15 | type NumverifyScannerResponse struct { 16 | Valid bool `json:"valid" console:"Valid"` 17 | Number string `json:"number" console:"Number,omitempty"` 18 | LocalFormat string `json:"local_format" console:"Local format,omitempty"` 19 | InternationalFormat string `json:"international_format" console:"International format,omitempty"` 20 | CountryPrefix string `json:"country_prefix" console:"Country prefix,omitempty"` 21 | CountryCode string `json:"country_code" console:"Country code,omitempty"` 22 | CountryName string `json:"country_name" console:"Country name,omitempty"` 23 | Location string `json:"location" console:"Location,omitempty"` 24 | Carrier string `json:"carrier" console:"Carrier,omitempty"` 25 | LineType string `json:"line_type" console:"Line type,omitempty"` 26 | } 27 | 28 | func NewNumverifyScanner(s suppliers.NumverifySupplierInterface) Scanner { 29 | return &numverifyScanner{client: s} 30 | } 31 | 32 | func (s *numverifyScanner) Name() string { 33 | return Numverify 34 | } 35 | 36 | func (s *numverifyScanner) Description() string { 37 | return "Request info about a given phone number through the Numverify API." 38 | } 39 | 40 | func (s *numverifyScanner) DryRun(_ number.Number, opts ScannerOptions) error { 41 | if opts.GetStringEnv("NUMVERIFY_API_KEY") != "" { 42 | return nil 43 | } 44 | return errors.New("API key is not defined") 45 | } 46 | 47 | func (s *numverifyScanner) Run(n number.Number, opts ScannerOptions) (interface{}, error) { 48 | apiKey := opts.GetStringEnv("NUMVERIFY_API_KEY") 49 | 50 | res, err := s.client.Request().SetApiKey(apiKey).ValidateNumber(n.International) 51 | if err != nil { 52 | return nil, err 53 | } 54 | 55 | data := NumverifyScannerResponse{ 56 | Valid: res.Valid, 57 | Number: res.Number, 58 | LocalFormat: res.LocalFormat, 59 | InternationalFormat: res.InternationalFormat, 60 | CountryPrefix: res.CountryPrefix, 61 | CountryCode: res.CountryCode, 62 | CountryName: res.CountryName, 63 | Location: res.Location, 64 | Carrier: res.Carrier, 65 | LineType: res.LineType, 66 | } 67 | 68 | return data, nil 69 | } 70 | -------------------------------------------------------------------------------- /lib/remote/numverify_scanner_test.go: -------------------------------------------------------------------------------- 1 | package remote_test 2 | 3 | import ( 4 | "errors" 5 | "github.com/stretchr/testify/assert" 6 | "github.com/sundowndev/phoneinfoga/v2/lib/filter" 7 | "github.com/sundowndev/phoneinfoga/v2/lib/number" 8 | "github.com/sundowndev/phoneinfoga/v2/lib/remote" 9 | "github.com/sundowndev/phoneinfoga/v2/lib/remote/suppliers" 10 | "github.com/sundowndev/phoneinfoga/v2/mocks" 11 | "testing" 12 | ) 13 | 14 | func TestNumverifyScanner_Metadata(t *testing.T) { 15 | scanner := remote.NewNumverifyScanner(&mocks.NumverifySupplier{}) 16 | assert.Equal(t, remote.Numverify, scanner.Name()) 17 | assert.NotEmpty(t, scanner.Description()) 18 | } 19 | 20 | func TestNumverifyScanner(t *testing.T) { 21 | dummyError := errors.New("dummy") 22 | 23 | testcases := []struct { 24 | name string 25 | number *number.Number 26 | opts remote.ScannerOptions 27 | mocks func(*mocks.NumverifySupplier, *mocks.NumverifySupplierReq) 28 | expected map[string]interface{} 29 | wantErrors map[string]error 30 | }{ 31 | { 32 | name: "successful scan", 33 | number: func() *number.Number { 34 | n, _ := number.NewNumber("15556661212") 35 | return n 36 | }(), 37 | opts: map[string]interface{}{ 38 | "NUMVERIFY_API_KEY": "secret", 39 | }, 40 | mocks: func(s *mocks.NumverifySupplier, r *mocks.NumverifySupplierReq) { 41 | s.On("Request").Return(r) 42 | r.On("SetApiKey", "secret").Return(r) 43 | r.On("ValidateNumber", "15556661212").Return(&suppliers.NumverifyValidateResponse{ 44 | Valid: true, 45 | Number: "test", 46 | LocalFormat: "test", 47 | InternationalFormat: "test", 48 | CountryPrefix: "test", 49 | CountryCode: "test", 50 | CountryName: "test", 51 | Location: "test", 52 | Carrier: "test", 53 | LineType: "test", 54 | }, nil).Once() 55 | }, 56 | expected: map[string]interface{}{ 57 | "numverify": remote.NumverifyScannerResponse{ 58 | Valid: true, 59 | Number: "test", 60 | LocalFormat: "test", 61 | InternationalFormat: "test", 62 | CountryPrefix: "test", 63 | CountryCode: "test", 64 | CountryName: "test", 65 | Location: "test", 66 | Carrier: "test", 67 | LineType: "test", 68 | }, 69 | }, 70 | wantErrors: map[string]error{}, 71 | }, 72 | { 73 | name: "failed scan", 74 | number: func() *number.Number { 75 | n, _ := number.NewNumber("15556661212") 76 | return n 77 | }(), 78 | opts: map[string]interface{}{ 79 | "NUMVERIFY_API_KEY": "secret", 80 | }, 81 | mocks: func(s *mocks.NumverifySupplier, r *mocks.NumverifySupplierReq) { 82 | s.On("Request").Return(r) 83 | r.On("SetApiKey", "secret").Return(r) 84 | r.On("ValidateNumber", "15556661212").Return(nil, dummyError).Once() 85 | }, 86 | expected: map[string]interface{}{}, 87 | wantErrors: map[string]error{ 88 | "numverify": dummyError, 89 | }, 90 | }, 91 | { 92 | name: "should not run", 93 | number: func() *number.Number { 94 | n, _ := number.NewNumber("15556661212") 95 | return n 96 | }(), 97 | mocks: func(s *mocks.NumverifySupplier, r *mocks.NumverifySupplierReq) {}, 98 | expected: map[string]interface{}{}, 99 | wantErrors: map[string]error{}, 100 | }, 101 | } 102 | 103 | for _, tt := range testcases { 104 | t.Run(tt.name, func(t *testing.T) { 105 | numverifySupplierMock := &mocks.NumverifySupplier{} 106 | numverifySupplierReqMock := &mocks.NumverifySupplierReq{} 107 | tt.mocks(numverifySupplierMock, numverifySupplierReqMock) 108 | 109 | scanner := remote.NewNumverifyScanner(numverifySupplierMock) 110 | lib := remote.NewLibrary(filter.NewEngine()) 111 | lib.AddScanner(scanner) 112 | 113 | got, errs := lib.Scan(tt.number, tt.opts) 114 | if len(tt.wantErrors) > 0 { 115 | assert.Equal(t, tt.wantErrors, errs) 116 | } else { 117 | assert.Len(t, errs, 0) 118 | } 119 | assert.Equal(t, tt.expected, got) 120 | 121 | numverifySupplierMock.AssertExpectations(t) 122 | }) 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /lib/remote/ovh_scanner.go: -------------------------------------------------------------------------------- 1 | package remote 2 | 3 | import ( 4 | "fmt" 5 | "github.com/sundowndev/phoneinfoga/v2/lib/number" 6 | "github.com/sundowndev/phoneinfoga/v2/lib/remote/suppliers" 7 | ) 8 | 9 | const OVH = "ovh" 10 | 11 | type ovhScanner struct { 12 | client suppliers.OVHSupplierInterface 13 | } 14 | 15 | // OVHScannerResponse is the OVH scanner response 16 | type OVHScannerResponse struct { 17 | Found bool `json:"found" console:"Found"` 18 | NumberRange string `json:"number_range,omitempty" console:"Number range,omitempty"` 19 | City string `json:"city,omitempty" console:"City,omitempty"` 20 | ZipCode string `json:"zip_code,omitempty" console:"Zip code,omitempty"` 21 | } 22 | 23 | func NewOVHScanner(s suppliers.OVHSupplierInterface) Scanner { 24 | return &ovhScanner{client: s} 25 | } 26 | 27 | func (s *ovhScanner) Name() string { 28 | return OVH 29 | } 30 | 31 | func (s *ovhScanner) Description() string { 32 | return "Search a phone number through the OVH Telecom REST API." 33 | } 34 | 35 | func (s *ovhScanner) DryRun(n number.Number, _ ScannerOptions) error { 36 | if !s.isSupported(n.CountryCode) { 37 | return fmt.Errorf("country code %d is not supported", n.CountryCode) 38 | } 39 | return nil 40 | } 41 | 42 | func (s *ovhScanner) Run(n number.Number, _ ScannerOptions) (interface{}, error) { 43 | res, err := s.client.Search(n) 44 | if err != nil { 45 | return nil, err 46 | } 47 | 48 | data := OVHScannerResponse{ 49 | Found: res.Found, 50 | NumberRange: res.NumberRange, 51 | City: res.City, 52 | ZipCode: res.ZipCode, 53 | } 54 | 55 | return data, nil 56 | } 57 | 58 | func (s *ovhScanner) supportedCountryCodes() []int32 { 59 | // See https://api.ovh.com/console/#/telephony/number/detailedZones#GET 60 | return []int32{33, 32, 44, 34, 41} 61 | } 62 | 63 | func (s *ovhScanner) isSupported(code int32) bool { 64 | supported := false 65 | for _, c := range s.supportedCountryCodes() { 66 | if code == c { 67 | supported = true 68 | } 69 | } 70 | return supported 71 | } 72 | -------------------------------------------------------------------------------- /lib/remote/ovh_scanner_test.go: -------------------------------------------------------------------------------- 1 | package remote_test 2 | 3 | import ( 4 | "errors" 5 | "github.com/stretchr/testify/assert" 6 | "github.com/sundowndev/phoneinfoga/v2/lib/filter" 7 | "github.com/sundowndev/phoneinfoga/v2/lib/number" 8 | "github.com/sundowndev/phoneinfoga/v2/lib/remote" 9 | "github.com/sundowndev/phoneinfoga/v2/lib/remote/suppliers" 10 | "github.com/sundowndev/phoneinfoga/v2/mocks" 11 | "testing" 12 | ) 13 | 14 | func TestOVHScanner_Metadata(t *testing.T) { 15 | scanner := remote.NewOVHScanner(&mocks.OVHSupplier{}) 16 | assert.Equal(t, remote.OVH, scanner.Name()) 17 | assert.NotEmpty(t, scanner.Description()) 18 | } 19 | 20 | func TestOVHScanner(t *testing.T) { 21 | dummyError := errors.New("dummy") 22 | 23 | dummyNumber, _ := number.NewNumber("33365174444") 24 | 25 | testcases := []struct { 26 | name string 27 | number *number.Number 28 | mocks func(s *mocks.OVHSupplier) 29 | expected map[string]interface{} 30 | wantErrors map[string]error 31 | }{ 32 | { 33 | name: "successful scan", 34 | number: func() *number.Number { 35 | return dummyNumber 36 | }(), 37 | mocks: func(s *mocks.OVHSupplier) { 38 | s.On("Search", *dummyNumber).Return(&suppliers.OVHScannerResponse{ 39 | Found: false, 40 | }, nil).Once() 41 | }, 42 | expected: map[string]interface{}{ 43 | "ovh": remote.OVHScannerResponse{ 44 | Found: false, 45 | }, 46 | }, 47 | wantErrors: map[string]error{}, 48 | }, 49 | { 50 | name: "failed scan", 51 | number: func() *number.Number { 52 | return dummyNumber 53 | }(), 54 | mocks: func(s *mocks.OVHSupplier) { 55 | s.On("Search", *dummyNumber).Return(nil, dummyError).Once() 56 | }, 57 | expected: map[string]interface{}{}, 58 | wantErrors: map[string]error{ 59 | "ovh": dummyError, 60 | }, 61 | }, 62 | { 63 | name: "country not supported", 64 | number: func() *number.Number { 65 | num, _ := number.NewNumber("15556661212") 66 | return num 67 | }(), 68 | mocks: func(s *mocks.OVHSupplier) {}, 69 | expected: map[string]interface{}{}, 70 | wantErrors: map[string]error{}, 71 | }, 72 | } 73 | 74 | for _, tt := range testcases { 75 | t.Run(tt.name, func(t *testing.T) { 76 | OVHSupplierMock := &mocks.OVHSupplier{} 77 | tt.mocks(OVHSupplierMock) 78 | 79 | scanner := remote.NewOVHScanner(OVHSupplierMock) 80 | lib := remote.NewLibrary(filter.NewEngine()) 81 | lib.AddScanner(scanner) 82 | 83 | got, errs := lib.Scan(tt.number, remote.ScannerOptions{}) 84 | if len(tt.wantErrors) > 0 { 85 | assert.Equal(t, tt.wantErrors, errs) 86 | } else { 87 | assert.Len(t, errs, 0) 88 | } 89 | assert.Equal(t, tt.expected, got) 90 | 91 | OVHSupplierMock.AssertExpectations(t) 92 | }) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /lib/remote/remote.go: -------------------------------------------------------------------------------- 1 | package remote 2 | 3 | import ( 4 | "errors" 5 | "github.com/sirupsen/logrus" 6 | "github.com/sundowndev/phoneinfoga/v2/lib/filter" 7 | "github.com/sundowndev/phoneinfoga/v2/lib/number" 8 | "sync" 9 | ) 10 | 11 | var mu sync.Locker = &sync.RWMutex{} 12 | var plugins []Scanner 13 | 14 | type Library struct { 15 | m *sync.RWMutex 16 | scanners []Scanner 17 | results map[string]interface{} 18 | errors map[string]error 19 | filter filter.Filter 20 | } 21 | 22 | func NewLibrary(filterEngine filter.Filter) *Library { 23 | return &Library{ 24 | m: &sync.RWMutex{}, 25 | scanners: []Scanner{}, 26 | results: map[string]interface{}{}, 27 | errors: map[string]error{}, 28 | filter: filterEngine, 29 | } 30 | } 31 | 32 | func (r *Library) LoadPlugins() { 33 | for _, s := range plugins { 34 | r.AddScanner(s) 35 | } 36 | } 37 | 38 | func (r *Library) AddScanner(s Scanner) { 39 | if r.filter.Match(s.Name()) { 40 | logrus.WithField("scanner", s.Name()).Debug("Scanner was ignored by filter") 41 | return 42 | } 43 | r.scanners = append(r.scanners, s) 44 | } 45 | 46 | func (r *Library) addResult(k string, v interface{}) { 47 | r.m.Lock() 48 | defer r.m.Unlock() 49 | r.results[k] = v 50 | } 51 | 52 | func (r *Library) addError(k string, err error) { 53 | r.m.Lock() 54 | defer r.m.Unlock() 55 | r.errors[k] = err 56 | } 57 | 58 | func (r *Library) Scan(n *number.Number, opts ScannerOptions) (map[string]interface{}, map[string]error) { 59 | var wg sync.WaitGroup 60 | 61 | for _, s := range r.scanners { 62 | wg.Add(1) 63 | go func(s Scanner) { 64 | defer wg.Done() 65 | defer func() { 66 | if err := recover(); err != nil { 67 | logrus.WithField("scanner", s.Name()).WithField("error", err).Debug("Scanner panicked") 68 | r.addError(s.Name(), errors.New("panic occurred while running scan, see debug logs")) 69 | } 70 | }() 71 | 72 | if err := s.DryRun(*n, opts); err != nil { 73 | logrus. 74 | WithField("scanner", s.Name()). 75 | WithField("reason", err.Error()). 76 | Debug("Scanner was ignored because it should not run") 77 | return 78 | } 79 | 80 | data, err := s.Run(*n, opts) 81 | if err != nil { 82 | r.addError(s.Name(), err) 83 | return 84 | } 85 | if data != nil { 86 | r.addResult(s.Name(), data) 87 | } 88 | }(s) 89 | } 90 | 91 | wg.Wait() 92 | 93 | return r.results, r.errors 94 | } 95 | 96 | func (r *Library) GetAllScanners() []Scanner { 97 | return r.scanners 98 | } 99 | 100 | func (r *Library) GetScanner(name string) Scanner { 101 | r.m.RLock() 102 | defer r.m.RUnlock() 103 | for _, s := range r.scanners { 104 | if s.Name() == name { 105 | return s 106 | } 107 | } 108 | return nil 109 | } 110 | 111 | func RegisterPlugin(s Scanner) { 112 | mu.Lock() 113 | defer mu.Unlock() 114 | plugins = append(plugins, s) 115 | } 116 | -------------------------------------------------------------------------------- /lib/remote/remote_test.go: -------------------------------------------------------------------------------- 1 | package remote_test 2 | 3 | import ( 4 | "errors" 5 | "github.com/stretchr/testify/assert" 6 | "github.com/sundowndev/phoneinfoga/v2/lib/filter" 7 | "github.com/sundowndev/phoneinfoga/v2/lib/number" 8 | "github.com/sundowndev/phoneinfoga/v2/lib/remote" 9 | "github.com/sundowndev/phoneinfoga/v2/mocks" 10 | "testing" 11 | ) 12 | 13 | func TestRemoteLibrary_SuccessScan(t *testing.T) { 14 | type fakeScannerResponse struct { 15 | Valid bool 16 | } 17 | 18 | expected := map[string]interface{}{ 19 | "fake": fakeScannerResponse{Valid: true}, 20 | "fake2": fakeScannerResponse{Valid: false}, 21 | } 22 | 23 | num, err := number.NewNumber("15556661212") 24 | if err != nil { 25 | t.Fatal(err) 26 | } 27 | 28 | fakeScanner := &mocks.Scanner{} 29 | fakeScanner.On("Name").Return("fake").Times(2) 30 | fakeScanner.On("DryRun", *num, remote.ScannerOptions{}).Return(nil).Once() 31 | fakeScanner.On("Run", *num, remote.ScannerOptions{}).Return(fakeScannerResponse{Valid: true}, nil).Once() 32 | 33 | fakeScanner2 := &mocks.Scanner{} 34 | fakeScanner2.On("Name").Return("fake2").Times(2) 35 | fakeScanner2.On("DryRun", *num, remote.ScannerOptions{}).Return(nil).Once() 36 | fakeScanner2.On("Run", *num, remote.ScannerOptions{}).Return(fakeScannerResponse{Valid: false}, nil).Once() 37 | 38 | lib := remote.NewLibrary(filter.NewEngine()) 39 | 40 | lib.AddScanner(fakeScanner) 41 | lib.AddScanner(fakeScanner2) 42 | 43 | result, errs := lib.Scan(num, remote.ScannerOptions{}) 44 | assert.Equal(t, expected, result) 45 | assert.Equal(t, map[string]error{}, errs) 46 | 47 | fakeScanner.AssertExpectations(t) 48 | fakeScanner2.AssertExpectations(t) 49 | } 50 | 51 | func TestRemoteLibrary_FailedScan(t *testing.T) { 52 | num, err := number.NewNumber("15556661212") 53 | if err != nil { 54 | t.Fatal(err) 55 | } 56 | 57 | dummyError := errors.New("test") 58 | 59 | fakeScanner := &mocks.Scanner{} 60 | fakeScanner.On("Name").Return("fake").Times(2) 61 | fakeScanner.On("DryRun", *num, remote.ScannerOptions{}).Return(nil).Once() 62 | fakeScanner.On("Run", *num, remote.ScannerOptions{}).Return(nil, dummyError).Once() 63 | 64 | lib := remote.NewLibrary(filter.NewEngine()) 65 | 66 | lib.AddScanner(fakeScanner) 67 | 68 | result, errs := lib.Scan(num, remote.ScannerOptions{}) 69 | assert.Equal(t, map[string]interface{}{}, result) 70 | assert.Equal(t, map[string]error{"fake": dummyError}, errs) 71 | 72 | fakeScanner.AssertExpectations(t) 73 | } 74 | 75 | func TestRemoteLibrary_EmptyScan(t *testing.T) { 76 | num, err := number.NewNumber("15556661212") 77 | if err != nil { 78 | t.Fatal(err) 79 | } 80 | 81 | fakeScanner := &mocks.Scanner{} 82 | fakeScanner.On("Name").Return("mockscanner").Times(2) 83 | fakeScanner.On("DryRun", *num, remote.ScannerOptions{}).Return(errors.New("dummy error")).Once() 84 | 85 | lib := remote.NewLibrary(filter.NewEngine()) 86 | 87 | lib.AddScanner(fakeScanner) 88 | 89 | result, errs := lib.Scan(num, remote.ScannerOptions{}) 90 | assert.Equal(t, map[string]interface{}{}, result) 91 | assert.Equal(t, map[string]error{}, errs) 92 | 93 | fakeScanner.AssertExpectations(t) 94 | } 95 | 96 | func TestRemoteLibrary_PanicRun(t *testing.T) { 97 | num, err := number.NewNumber("15556661212") 98 | if err != nil { 99 | t.Fatal(err) 100 | } 101 | 102 | fakeScanner := &mocks.Scanner{} 103 | fakeScanner.On("Name").Return("fake") 104 | fakeScanner.On("DryRun", *num, remote.ScannerOptions{}).Return(nil).Once() 105 | fakeScanner.On("Run", *num, remote.ScannerOptions{}).Panic("dummy panic").Once() 106 | 107 | lib := remote.NewLibrary(filter.NewEngine()) 108 | 109 | lib.AddScanner(fakeScanner) 110 | 111 | result, errs := lib.Scan(num, remote.ScannerOptions{}) 112 | assert.Equal(t, map[string]interface{}{}, result) 113 | assert.Equal(t, map[string]error{"fake": errors.New("panic occurred while running scan, see debug logs")}, errs) 114 | 115 | fakeScanner.AssertExpectations(t) 116 | } 117 | 118 | func TestRemoteLibrary_PanicDryRun(t *testing.T) { 119 | num, err := number.NewNumber("15556661212") 120 | if err != nil { 121 | t.Fatal(err) 122 | } 123 | 124 | fakeScanner := &mocks.Scanner{} 125 | fakeScanner.On("Name").Return("fake") 126 | fakeScanner.On("DryRun", *num, remote.ScannerOptions{}).Panic("dummy panic").Once() 127 | 128 | lib := remote.NewLibrary(filter.NewEngine()) 129 | 130 | lib.AddScanner(fakeScanner) 131 | 132 | result, errs := lib.Scan(num, remote.ScannerOptions{}) 133 | assert.Equal(t, map[string]interface{}{}, result) 134 | assert.Equal(t, map[string]error{"fake": errors.New("panic occurred while running scan, see debug logs")}, errs) 135 | 136 | fakeScanner.AssertExpectations(t) 137 | } 138 | 139 | func TestRemoteLibrary_GetAllScanners(t *testing.T) { 140 | fakeScanner := &mocks.Scanner{} 141 | fakeScanner.On("Name").Return("fake") 142 | 143 | fakeScanner2 := &mocks.Scanner{} 144 | fakeScanner2.On("Name").Return("fake2") 145 | 146 | lib := remote.NewLibrary(filter.NewEngine()) 147 | 148 | lib.AddScanner(fakeScanner) 149 | lib.AddScanner(fakeScanner2) 150 | 151 | assert.Equal(t, []remote.Scanner{fakeScanner, fakeScanner2}, lib.GetAllScanners()) 152 | } 153 | 154 | func TestRemoteLibrary_AddIgnoredScanner(t *testing.T) { 155 | fakeScanner := &mocks.Scanner{} 156 | fakeScanner.On("Name").Return("fake") 157 | 158 | fakeScanner2 := &mocks.Scanner{} 159 | fakeScanner2.On("Name").Return("fake2") 160 | 161 | f := filter.NewEngine() 162 | f.AddRule("fake2") 163 | lib := remote.NewLibrary(f) 164 | 165 | lib.AddScanner(fakeScanner) 166 | lib.AddScanner(fakeScanner2) 167 | 168 | assert.Equal(t, []remote.Scanner{fakeScanner}, lib.GetAllScanners()) 169 | } 170 | -------------------------------------------------------------------------------- /lib/remote/scanner.go: -------------------------------------------------------------------------------- 1 | package remote 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "plugin" 7 | 8 | "github.com/sundowndev/phoneinfoga/v2/lib/number" 9 | ) 10 | 11 | type ScannerOptions map[string]interface{} 12 | 13 | func (o ScannerOptions) GetStringEnv(k string) string { 14 | if v, ok := o[k].(string); ok { 15 | return v 16 | } 17 | return os.Getenv(k) 18 | } 19 | 20 | type Plugin interface { 21 | Lookup(string) (plugin.Symbol, error) 22 | } 23 | 24 | type Scanner interface { 25 | Name() string 26 | Description() string 27 | DryRun(number.Number, ScannerOptions) error 28 | Run(number.Number, ScannerOptions) (interface{}, error) 29 | } 30 | 31 | func OpenPlugin(path string) error { 32 | if _, err := os.Stat(path); os.IsNotExist(err) { 33 | return fmt.Errorf("given path %s does not exist", path) 34 | } 35 | 36 | _, err := plugin.Open(path) 37 | if err != nil { 38 | return fmt.Errorf("given plugin %s is not valid: %v", path, err) 39 | } 40 | 41 | return nil 42 | } 43 | -------------------------------------------------------------------------------- /lib/remote/scanner_test.go: -------------------------------------------------------------------------------- 1 | package remote 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func Test_ValidatePlugin_Errors(t *testing.T) { 13 | invalidPluginAbsPath, err := filepath.Abs("testdata/invalid.so") 14 | if err != nil { 15 | assert.FailNow(t, "failed to get the absolute path of test file: %v", err) 16 | } 17 | 18 | testcases := []struct { 19 | name string 20 | path string 21 | wantErr string 22 | }{ 23 | { 24 | name: "test with invalid path", 25 | path: "testdata/doesnotexist", 26 | wantErr: "given path testdata/doesnotexist does not exist", 27 | }, 28 | { 29 | name: "test with invalid plugin", 30 | path: "testdata/invalid.so", 31 | wantErr: fmt.Sprintf("given plugin testdata/invalid.so is not valid: plugin.Open(\"testdata/invalid.so\"): %s: file too short", invalidPluginAbsPath), 32 | }, 33 | } 34 | 35 | for _, tt := range testcases { 36 | t.Run(tt.name, func(t *testing.T) { 37 | err := OpenPlugin(tt.path) 38 | assert.EqualError(t, err, tt.wantErr) 39 | }) 40 | } 41 | } 42 | 43 | func TestScannerOptions(t *testing.T) { 44 | testcases := []struct { 45 | name string 46 | opts ScannerOptions 47 | check func(*testing.T, ScannerOptions) 48 | }{ 49 | { 50 | name: "test GetStringEnv with simple options", 51 | opts: map[string]interface{}{ 52 | "foo": "bar", 53 | }, 54 | check: func(t *testing.T, opts ScannerOptions) { 55 | assert.Equal(t, opts.GetStringEnv("foo"), "bar") 56 | assert.Equal(t, opts.GetStringEnv("bar"), "") 57 | }, 58 | }, 59 | { 60 | name: "test GetStringEnv with env vars", 61 | opts: map[string]interface{}{ 62 | "foo_bar": "bar", 63 | }, 64 | check: func(t *testing.T, opts ScannerOptions) { 65 | _ = os.Setenv("FOO_BAR", "secret") 66 | defer os.Unsetenv("FOO_BAR") 67 | 68 | assert.Equal(t, opts.GetStringEnv("FOO_BAR"), "secret") 69 | assert.Equal(t, opts.GetStringEnv("foo_bar"), "bar") 70 | assert.Equal(t, opts.GetStringEnv("foo"), "") 71 | }, 72 | }, 73 | } 74 | 75 | for _, tt := range testcases { 76 | t.Run(tt.name, func(t *testing.T) { 77 | tt.check(t, tt.opts) 78 | }) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /lib/remote/suppliers/numverify.go: -------------------------------------------------------------------------------- 1 | package suppliers 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "github.com/sirupsen/logrus" 8 | "net/http" 9 | ) 10 | 11 | type NumverifySupplierInterface interface { 12 | Request() NumverifySupplierRequestInterface 13 | } 14 | 15 | type NumverifySupplierRequestInterface interface { 16 | SetApiKey(string) NumverifySupplierRequestInterface 17 | ValidateNumber(string) (*NumverifyValidateResponse, error) 18 | } 19 | 20 | type NumverifyErrorResponse struct { 21 | Message string `json:"message"` 22 | } 23 | 24 | // NumverifyValidateResponse REST API response 25 | type NumverifyValidateResponse struct { 26 | Valid bool `json:"valid"` 27 | Number string `json:"number"` 28 | LocalFormat string `json:"local_format"` 29 | InternationalFormat string `json:"international_format"` 30 | CountryPrefix string `json:"country_prefix"` 31 | CountryCode string `json:"country_code"` 32 | CountryName string `json:"country_name"` 33 | Location string `json:"location"` 34 | Carrier string `json:"carrier"` 35 | LineType string `json:"line_type"` 36 | } 37 | 38 | type NumverifySupplier struct { 39 | Uri string 40 | } 41 | 42 | func NewNumverifySupplier() *NumverifySupplier { 43 | return &NumverifySupplier{ 44 | Uri: "https://api.apilayer.com", 45 | } 46 | } 47 | 48 | type NumverifyRequest struct { 49 | apiKey string 50 | uri string 51 | } 52 | 53 | func (s *NumverifySupplier) Request() NumverifySupplierRequestInterface { 54 | return &NumverifyRequest{uri: s.Uri} 55 | } 56 | 57 | func (r *NumverifyRequest) SetApiKey(k string) NumverifySupplierRequestInterface { 58 | r.apiKey = k 59 | return r 60 | } 61 | 62 | func (r *NumverifyRequest) ValidateNumber(internationalNumber string) (res *NumverifyValidateResponse, err error) { 63 | logrus. 64 | WithField("number", internationalNumber). 65 | Debug("Running validate operation through Numverify API") 66 | 67 | url := fmt.Sprintf("%s/number_verification/validate?number=%s", r.uri, internationalNumber) 68 | 69 | // Build the request 70 | client := &http.Client{} 71 | req, _ := http.NewRequest("GET", url, nil) 72 | req.Header.Set("Apikey", r.apiKey) 73 | 74 | response, err := client.Do(req) 75 | 76 | if err != nil { 77 | return nil, err 78 | } 79 | defer response.Body.Close() 80 | 81 | // Fill the response with the data from the JSON 82 | var result NumverifyValidateResponse 83 | 84 | if response.StatusCode >= 400 { 85 | errorResponse := NumverifyErrorResponse{} 86 | if err := json.NewDecoder(response.Body).Decode(&errorResponse); err != nil { 87 | return nil, err 88 | } 89 | return nil, errors.New(errorResponse.Message) 90 | } 91 | 92 | // Use json.Decode for reading streams of JSON data 93 | if err := json.NewDecoder(response.Body).Decode(&result); err != nil { 94 | return nil, err 95 | } 96 | 97 | res = &NumverifyValidateResponse{ 98 | Valid: result.Valid, 99 | Number: result.Number, 100 | LocalFormat: result.LocalFormat, 101 | InternationalFormat: result.InternationalFormat, 102 | CountryPrefix: result.CountryPrefix, 103 | CountryCode: result.CountryCode, 104 | CountryName: result.CountryName, 105 | Location: result.Location, 106 | Carrier: result.Carrier, 107 | LineType: result.LineType, 108 | } 109 | 110 | return res, nil 111 | } 112 | -------------------------------------------------------------------------------- /lib/remote/suppliers/numverify_test.go: -------------------------------------------------------------------------------- 1 | package suppliers 2 | 3 | import ( 4 | "errors" 5 | "github.com/stretchr/testify/assert" 6 | "gopkg.in/h2non/gock.v1" 7 | "net/url" 8 | "os" 9 | "testing" 10 | ) 11 | 12 | func TestNumverifySupplierSuccessCustomApiKey(t *testing.T) { 13 | defer gock.Off() // Flush pending mocks after test execution 14 | 15 | number := "11115551212" 16 | apikey := "5ad5554ac240e4d3d31107941b35a5eb" 17 | 18 | expectedResult := &NumverifyValidateResponse{ 19 | Valid: true, 20 | Number: "79516566591", 21 | LocalFormat: "9516566591", 22 | InternationalFormat: "+79516566591", 23 | CountryPrefix: "+7", 24 | CountryCode: "RU", 25 | CountryName: "Russian Federation", 26 | Location: "Saint Petersburg and Leningrad Oblast", 27 | Carrier: "OJSC St. Petersburg Telecom (OJSC Tele2-Saint-Petersburg)", 28 | LineType: "mobile", 29 | } 30 | 31 | gock.New("https://api.apilayer.com"). 32 | Get("/number_verification/validate"). 33 | MatchHeader("Apikey", apikey). 34 | MatchParam("number", number). 35 | Reply(200). 36 | JSON(expectedResult) 37 | 38 | s := NewNumverifySupplier() 39 | 40 | got, err := s.Request().SetApiKey(apikey).ValidateNumber(number) 41 | assert.Nil(t, err) 42 | 43 | assert.Equal(t, expectedResult, got) 44 | } 45 | 46 | func TestNumverifySupplierError(t *testing.T) { 47 | defer gock.Off() // Flush pending mocks after test execution 48 | 49 | number := "11115551212" 50 | apikey := "5ad5554ac240e4d3d31107941b35a5eb" 51 | 52 | expectedResult := &NumverifyErrorResponse{ 53 | Message: "You have exceeded your daily\\/monthly API rate limit. Please review and upgrade your subscription plan at https:\\/\\/apilayer.com\\/subscriptions to continue.", 54 | } 55 | 56 | gock.New("https://api.apilayer.com"). 57 | Get("/number_verification/validate"). 58 | MatchHeader("Apikey", apikey). 59 | MatchParam("number", number). 60 | Reply(429). 61 | JSON(expectedResult) 62 | 63 | s := NewNumverifySupplier() 64 | 65 | got, err := s.Request().SetApiKey(apikey).ValidateNumber(number) 66 | assert.Nil(t, got) 67 | assert.Equal(t, errors.New("You have exceeded your daily\\/monthly API rate limit. Please review and upgrade your subscription plan at https:\\/\\/apilayer.com\\/subscriptions to continue."), err) 68 | } 69 | 70 | func TestNumverifySupplierHTTPError(t *testing.T) { 71 | defer gock.Off() // Flush pending mocks after test execution 72 | 73 | number := "11115551212" 74 | 75 | _ = os.Setenv("NUMVERIFY_API_KEY", "5ad5554ac240e4d3d31107941b35a5eb") 76 | defer os.Clearenv() 77 | 78 | dummyError := errors.New("test") 79 | 80 | gock.New("https://api.apilayer.com"). 81 | Get("/number_verification/validate"). 82 | ReplyError(dummyError) 83 | 84 | s := NewNumverifySupplier() 85 | 86 | got, err := s.Request().ValidateNumber(number) 87 | assert.Nil(t, got) 88 | assert.Equal(t, &url.Error{ 89 | Op: "Get", 90 | URL: "https://api.apilayer.com/number_verification/validate?number=11115551212", 91 | Err: dummyError, 92 | }, err) 93 | } 94 | -------------------------------------------------------------------------------- /lib/remote/suppliers/ovh.go: -------------------------------------------------------------------------------- 1 | package suppliers 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "github.com/sundowndev/phoneinfoga/v2/lib/number" 8 | "net/http" 9 | "reflect" 10 | "strings" 11 | ) 12 | 13 | type OVHSupplierInterface interface { 14 | Search(number.Number) (*OVHScannerResponse, error) 15 | } 16 | 17 | // OVHAPIResponseNumber is a type that describes an OVH number range 18 | type OVHAPIResponseNumber struct { 19 | MatchingCriteria interface{} `json:"matchingCriteria"` 20 | City string `json:"city"` 21 | ZneList []string `json:"zne-list"` 22 | InternationalNumber string `json:"internationalNumber"` 23 | Country string `json:"country"` 24 | AskedCity interface{} `json:"askedCity"` 25 | ZipCode string `json:"zipCode"` 26 | Number string `json:"number"` 27 | Prefix int `json:"prefix"` 28 | } 29 | 30 | type OVHAPIErrorResponse struct { 31 | Message string `json:"message"` 32 | } 33 | 34 | // OVHScannerResponse is the OVH scanner response 35 | type OVHScannerResponse struct { 36 | Found bool 37 | NumberRange string 38 | City string 39 | ZipCode string 40 | } 41 | 42 | type OVHSupplier struct{} 43 | 44 | func NewOVHSupplier() *OVHSupplier { 45 | return &OVHSupplier{} 46 | } 47 | 48 | func (s *OVHSupplier) Search(num number.Number) (*OVHScannerResponse, error) { 49 | countryCode := strings.ToLower(num.Country) 50 | 51 | if countryCode == "" { 52 | return nil, fmt.Errorf("country code +%d wasn't recognized", num.CountryCode) 53 | } 54 | 55 | // Build the request 56 | response, err := http.Get(fmt.Sprintf("https://api.ovh.com/1.0/telephony/number/detailedZones?country=%s", countryCode)) 57 | if err != nil { 58 | return nil, err 59 | } 60 | defer response.Body.Close() 61 | 62 | if response.StatusCode >= 400 { 63 | var result OVHAPIErrorResponse 64 | err = json.NewDecoder(response.Body).Decode(&result) 65 | if err != nil { 66 | return nil, err 67 | } 68 | return nil, errors.New(result.Message) 69 | } 70 | 71 | // Fill the response with the data from the JSON 72 | var results []OVHAPIResponseNumber 73 | 74 | // Use json.Decode for reading streams of JSON data 75 | err = json.NewDecoder(response.Body).Decode(&results) 76 | if err != nil { 77 | return nil, err 78 | } 79 | 80 | var foundNumber OVHAPIResponseNumber 81 | 82 | rt := reflect.TypeOf(results) 83 | if rt.Kind() == reflect.Slice && len(num.RawLocal) > 6 { 84 | askedNumber := num.RawLocal[0:6] + "xxxx" 85 | 86 | for _, result := range results { 87 | if result.Number == askedNumber { 88 | foundNumber = result 89 | } 90 | } 91 | } 92 | 93 | return &OVHScannerResponse{ 94 | Found: len(foundNumber.Number) > 0, 95 | NumberRange: foundNumber.Number, 96 | City: foundNumber.City, 97 | ZipCode: foundNumber.ZipCode, 98 | }, nil 99 | } 100 | -------------------------------------------------------------------------------- /lib/remote/suppliers/ovh_test.go: -------------------------------------------------------------------------------- 1 | package suppliers 2 | 3 | import ( 4 | "errors" 5 | "github.com/stretchr/testify/assert" 6 | "github.com/sundowndev/phoneinfoga/v2/lib/number" 7 | "gopkg.in/h2non/gock.v1" 8 | "net/url" 9 | "testing" 10 | ) 11 | 12 | func TestOVHSupplierSuccess(t *testing.T) { 13 | defer gock.Off() // Flush pending mocks after test execution 14 | 15 | num, _ := number.NewNumber("33365172812") 16 | 17 | gock.New("https://api.ovh.com"). 18 | Get("/1.0/telephony/number/detailedZones"). 19 | MatchParam("country", "fr"). 20 | Reply(200). 21 | JSON([]OVHAPIResponseNumber{ 22 | { 23 | ZneList: []string{}, 24 | MatchingCriteria: "", 25 | Prefix: 33, 26 | InternationalNumber: "003336517xxxx", 27 | Country: "fr", 28 | ZipCode: "", 29 | Number: "036517xxxx", 30 | City: "Abbeville", 31 | AskedCity: "", 32 | }, 33 | }) 34 | 35 | s := NewOVHSupplier() 36 | 37 | got, err := s.Search(*num) 38 | assert.Nil(t, err) 39 | 40 | expectedResult := &OVHScannerResponse{ 41 | Found: true, 42 | NumberRange: "036517xxxx", 43 | City: "Abbeville", 44 | } 45 | 46 | assert.Equal(t, expectedResult, got) 47 | } 48 | 49 | func TestOVHSupplierError(t *testing.T) { 50 | defer gock.Off() // Flush pending mocks after test execution 51 | 52 | num, _ := number.NewNumber("33365172812") 53 | 54 | dummyError := errors.New("test") 55 | 56 | gock.New("https://api.ovh.com"). 57 | Get("/1.0/telephony/number/detailedZones"). 58 | MatchParam("country", "fr"). 59 | ReplyError(dummyError) 60 | 61 | s := NewOVHSupplier() 62 | 63 | got, err := s.Search(*num) 64 | assert.Nil(t, got) 65 | assert.Equal(t, &url.Error{ 66 | Op: "Get", 67 | URL: "https://api.ovh.com/1.0/telephony/number/detailedZones?country=fr", 68 | Err: dummyError, 69 | }, err) 70 | } 71 | 72 | func TestOVHSupplierCountryCodeError(t *testing.T) { 73 | defer gock.Off() // Flush pending mocks after test execution 74 | 75 | gock.New("https://api.ovh.com"). 76 | Get("/1.0/telephony/number/detailedZones"). 77 | MatchParam("country", "co"). 78 | Reply(400). 79 | JSON(OVHAPIErrorResponse{Message: "[country] Given data (co) does not belong to the NumberCountryEnum enumeration"}) 80 | 81 | num, err := number.NewNumber("+575556661212") 82 | if err != nil { 83 | t.Fatal(err) 84 | } 85 | 86 | s := NewOVHSupplier() 87 | 88 | got, err := s.Search(*num) 89 | assert.Nil(t, got) 90 | assert.EqualError(t, err, "[country] Given data (co) does not belong to the NumberCountryEnum enumeration") 91 | } 92 | -------------------------------------------------------------------------------- /lib/remote/testdata/.gitignore: -------------------------------------------------------------------------------- 1 | !*.so 2 | -------------------------------------------------------------------------------- /lib/remote/testdata/invalid.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sundowndev/phoneinfoga/5f6156f567613a07ea8e3f04b004864582060a57/lib/remote/testdata/invalid.so -------------------------------------------------------------------------------- /logs/config.go: -------------------------------------------------------------------------------- 1 | package logs 2 | 3 | import ( 4 | "github.com/sirupsen/logrus" 5 | "github.com/sundowndev/phoneinfoga/v2/build" 6 | "os" 7 | ) 8 | 9 | type Config struct { 10 | Level logrus.Level 11 | ReportCaller bool 12 | } 13 | 14 | func getConfig() Config { 15 | config := Config{ 16 | Level: logrus.WarnLevel, 17 | ReportCaller: false, 18 | } 19 | 20 | if !build.IsRelease() { 21 | config.Level = logrus.DebugLevel 22 | } 23 | 24 | if lvl := os.Getenv("LOG_LEVEL"); lvl != "" { 25 | loglevel, _ := logrus.ParseLevel(lvl) 26 | config.Level = loglevel 27 | } 28 | 29 | return config 30 | } 31 | -------------------------------------------------------------------------------- /logs/init.go: -------------------------------------------------------------------------------- 1 | package logs 2 | 3 | import ( 4 | "github.com/sirupsen/logrus" 5 | ) 6 | 7 | func Init() { 8 | config := getConfig() 9 | logrus.SetLevel(config.Level) 10 | logrus.SetReportCaller(config.ReportCaller) 11 | } 12 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/sirupsen/logrus" 6 | "github.com/sundowndev/phoneinfoga/v2/build" 7 | "github.com/sundowndev/phoneinfoga/v2/cmd" 8 | "github.com/sundowndev/phoneinfoga/v2/logs" 9 | ) 10 | 11 | func main() { 12 | logs.Init() 13 | logrus.WithFields(logrus.Fields{ 14 | "isRelease": fmt.Sprintf("%t", build.IsRelease()), 15 | "version": build.String(), 16 | }).Debug("Build info") 17 | cmd.Execute() 18 | } 19 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: PhoneInfoga 2 | site_url: https://sundowndev.github.io/phoneinfoga 3 | repo_name: 'sundowndev/phoneinfoga' 4 | repo_url: 'https://github.com/sundowndev/phoneinfoga' 5 | site_description: 'Advanced information gathering & OSINT tool for phone numbers.' 6 | site_author: 'Sundowndev' 7 | copyright: 'PhoneInfoga was developed by sundowndev and is licensed under GPL-3.0.' 8 | nav: 9 | - 'Home': index.md 10 | - 'Getting Started': 11 | - 'Installation': getting-started/install.md 12 | - 'Usage': getting-started/usage.md 13 | - 'Scanners': getting-started/scanners.md 14 | - 'Go module usage': getting-started/go-module-usage.md 15 | - 'Resources': 16 | - 'Formatting phone numbers': resources/formatting.md 17 | - 'Additional resources': resources/additional-resources.md 18 | - 'Contribute': contribute.md 19 | theme: 20 | name: material 21 | logo: './images/logo_white.svg' 22 | favicon: './images/logo.svg' 23 | palette: 24 | - scheme: default 25 | primary: blue grey 26 | accent: indigo 27 | toggle: 28 | icon: material/brightness-7 29 | name: Switch to dark mode 30 | - scheme: slate 31 | primary: blue grey 32 | accent: indigo 33 | toggle: 34 | icon: material/brightness-4 35 | name: Switch to light mode 36 | features: 37 | - content.tabs.link 38 | - navigation.instant 39 | - navigation.sections 40 | - navigation.tabs 41 | extra: 42 | social: 43 | - icon: fontawesome/brands/github-alt 44 | link: 'https://github.com/sundowndev/phoneinfoga' 45 | 46 | # Extensions 47 | markdown_extensions: 48 | - markdown.extensions.admonition 49 | - pymdownx.superfences 50 | - pymdownx.tabbed: 51 | alternate_style: true 52 | - attr_list 53 | - admonition 54 | - pymdownx.details 55 | plugins: 56 | - search 57 | - minify: 58 | minify_html: true 59 | - redirects: 60 | redirect_maps: 61 | 'install.md': 'getting-started/install.md' 62 | 'usage.md': 'getting-started/usage.md' 63 | 'scanners.md': 'getting-started/scanners.md' 64 | 'go-module-usage.md': 'getting-started/go-module-usage.md' 65 | 'formatting.md': 'resources/formatting.md' 66 | 'resources.md': 'resources/additional-resources.md' 67 | -------------------------------------------------------------------------------- /mocks/NumverifySupplier.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.38.0. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import ( 6 | mock "github.com/stretchr/testify/mock" 7 | suppliers "github.com/sundowndev/phoneinfoga/v2/lib/remote/suppliers" 8 | ) 9 | 10 | // NumverifySupplier is an autogenerated mock type for the NumverifySupplierInterface type 11 | type NumverifySupplier struct { 12 | mock.Mock 13 | } 14 | 15 | // Request provides a mock function with given fields: 16 | func (_m *NumverifySupplier) Request() suppliers.NumverifySupplierRequestInterface { 17 | ret := _m.Called() 18 | 19 | if len(ret) == 0 { 20 | panic("no return value specified for Request") 21 | } 22 | 23 | var r0 suppliers.NumverifySupplierRequestInterface 24 | if rf, ok := ret.Get(0).(func() suppliers.NumverifySupplierRequestInterface); ok { 25 | r0 = rf() 26 | } else { 27 | if ret.Get(0) != nil { 28 | r0 = ret.Get(0).(suppliers.NumverifySupplierRequestInterface) 29 | } 30 | } 31 | 32 | return r0 33 | } 34 | 35 | // NewNumverifySupplier creates a new instance of NumverifySupplier. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 36 | // The first argument is typically a *testing.T value. 37 | func NewNumverifySupplier(t interface { 38 | mock.TestingT 39 | Cleanup(func()) 40 | }) *NumverifySupplier { 41 | mock := &NumverifySupplier{} 42 | mock.Mock.Test(t) 43 | 44 | t.Cleanup(func() { mock.AssertExpectations(t) }) 45 | 46 | return mock 47 | } 48 | -------------------------------------------------------------------------------- /mocks/NumverifySupplierRequest.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.38.0. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import ( 6 | mock "github.com/stretchr/testify/mock" 7 | suppliers "github.com/sundowndev/phoneinfoga/v2/lib/remote/suppliers" 8 | ) 9 | 10 | // NumverifySupplierReq is an autogenerated mock type for the NumverifySupplierRequestInterface type 11 | type NumverifySupplierReq struct { 12 | mock.Mock 13 | } 14 | 15 | // SetApiKey provides a mock function with given fields: _a0 16 | func (_m *NumverifySupplierReq) SetApiKey(_a0 string) suppliers.NumverifySupplierRequestInterface { 17 | ret := _m.Called(_a0) 18 | 19 | if len(ret) == 0 { 20 | panic("no return value specified for SetApiKey") 21 | } 22 | 23 | var r0 suppliers.NumverifySupplierRequestInterface 24 | if rf, ok := ret.Get(0).(func(string) suppliers.NumverifySupplierRequestInterface); ok { 25 | r0 = rf(_a0) 26 | } else { 27 | if ret.Get(0) != nil { 28 | r0 = ret.Get(0).(suppliers.NumverifySupplierRequestInterface) 29 | } 30 | } 31 | 32 | return r0 33 | } 34 | 35 | // ValidateNumber provides a mock function with given fields: _a0 36 | func (_m *NumverifySupplierReq) ValidateNumber(_a0 string) (*suppliers.NumverifyValidateResponse, error) { 37 | ret := _m.Called(_a0) 38 | 39 | if len(ret) == 0 { 40 | panic("no return value specified for ValidateNumber") 41 | } 42 | 43 | var r0 *suppliers.NumverifyValidateResponse 44 | var r1 error 45 | if rf, ok := ret.Get(0).(func(string) (*suppliers.NumverifyValidateResponse, error)); ok { 46 | return rf(_a0) 47 | } 48 | if rf, ok := ret.Get(0).(func(string) *suppliers.NumverifyValidateResponse); ok { 49 | r0 = rf(_a0) 50 | } else { 51 | if ret.Get(0) != nil { 52 | r0 = ret.Get(0).(*suppliers.NumverifyValidateResponse) 53 | } 54 | } 55 | 56 | if rf, ok := ret.Get(1).(func(string) error); ok { 57 | r1 = rf(_a0) 58 | } else { 59 | r1 = ret.Error(1) 60 | } 61 | 62 | return r0, r1 63 | } 64 | 65 | // NewNumverifySupplierReq creates a new instance of NumverifySupplierReq. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 66 | // The first argument is typically a *testing.T value. 67 | func NewNumverifySupplierReq(t interface { 68 | mock.TestingT 69 | Cleanup(func()) 70 | }) *NumverifySupplierReq { 71 | mock := &NumverifySupplierReq{} 72 | mock.Mock.Test(t) 73 | 74 | t.Cleanup(func() { mock.AssertExpectations(t) }) 75 | 76 | return mock 77 | } 78 | -------------------------------------------------------------------------------- /mocks/OVHSupplierInterface.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.8.0. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import ( 6 | mock "github.com/stretchr/testify/mock" 7 | number "github.com/sundowndev/phoneinfoga/v2/lib/number" 8 | suppliers "github.com/sundowndev/phoneinfoga/v2/lib/remote/suppliers" 9 | ) 10 | 11 | // OVHSupplier is an autogenerated mock type for the OVHSupplierInterface type 12 | type OVHSupplier struct { 13 | mock.Mock 14 | } 15 | 16 | // Search provides a mock function with given fields: _a0 17 | func (_m *OVHSupplier) Search(_a0 number.Number) (*suppliers.OVHScannerResponse, error) { 18 | ret := _m.Called(_a0) 19 | 20 | var r0 *suppliers.OVHScannerResponse 21 | if rf, ok := ret.Get(0).(func(number.Number) *suppliers.OVHScannerResponse); ok { 22 | r0 = rf(_a0) 23 | } else { 24 | if ret.Get(0) != nil { 25 | r0 = ret.Get(0).(*suppliers.OVHScannerResponse) 26 | } 27 | } 28 | 29 | var r1 error 30 | if rf, ok := ret.Get(1).(func(number.Number) error); ok { 31 | r1 = rf(_a0) 32 | } else { 33 | r1 = ret.Error(1) 34 | } 35 | 36 | return r0, r1 37 | } 38 | -------------------------------------------------------------------------------- /mocks/Plugin.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.8.0. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import ( 6 | plugin "plugin" 7 | 8 | mock "github.com/stretchr/testify/mock" 9 | ) 10 | 11 | // Plugin is an autogenerated mock type for the Plugin type 12 | type Plugin struct { 13 | mock.Mock 14 | } 15 | 16 | // Lookup provides a mock function with given fields: _a0 17 | func (_m *Plugin) Lookup(_a0 string) (plugin.Symbol, error) { 18 | ret := _m.Called(_a0) 19 | 20 | var r0 plugin.Symbol 21 | if rf, ok := ret.Get(0).(func(string) plugin.Symbol); ok { 22 | r0 = rf(_a0) 23 | } else { 24 | if ret.Get(0) != nil { 25 | r0 = ret.Get(0).(plugin.Symbol) 26 | } 27 | } 28 | 29 | var r1 error 30 | if rf, ok := ret.Get(1).(func(string) error); ok { 31 | r1 = rf(_a0) 32 | } else { 33 | r1 = ret.Error(1) 34 | } 35 | 36 | return r0, r1 37 | } 38 | -------------------------------------------------------------------------------- /mocks/Scanner.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.38.0. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import ( 6 | mock "github.com/stretchr/testify/mock" 7 | number "github.com/sundowndev/phoneinfoga/v2/lib/number" 8 | remote "github.com/sundowndev/phoneinfoga/v2/lib/remote" 9 | ) 10 | 11 | // Scanner is an autogenerated mock type for the Scanner type 12 | type Scanner struct { 13 | mock.Mock 14 | } 15 | 16 | // Description provides a mock function with given fields: 17 | func (_m *Scanner) Description() string { 18 | ret := _m.Called() 19 | 20 | if len(ret) == 0 { 21 | panic("no return value specified for Description") 22 | } 23 | 24 | var r0 string 25 | if rf, ok := ret.Get(0).(func() string); ok { 26 | r0 = rf() 27 | } else { 28 | r0 = ret.Get(0).(string) 29 | } 30 | 31 | return r0 32 | } 33 | 34 | // DryRun provides a mock function with given fields: _a0, _a1 35 | func (_m *Scanner) DryRun(_a0 number.Number, _a1 remote.ScannerOptions) error { 36 | ret := _m.Called(_a0, _a1) 37 | 38 | if len(ret) == 0 { 39 | panic("no return value specified for DryRun") 40 | } 41 | 42 | var r0 error 43 | if rf, ok := ret.Get(0).(func(number.Number, remote.ScannerOptions) error); ok { 44 | r0 = rf(_a0, _a1) 45 | } else { 46 | r0 = ret.Error(0) 47 | } 48 | 49 | return r0 50 | } 51 | 52 | // Name provides a mock function with given fields: 53 | func (_m *Scanner) Name() string { 54 | ret := _m.Called() 55 | 56 | if len(ret) == 0 { 57 | panic("no return value specified for Name") 58 | } 59 | 60 | var r0 string 61 | if rf, ok := ret.Get(0).(func() string); ok { 62 | r0 = rf() 63 | } else { 64 | r0 = ret.Get(0).(string) 65 | } 66 | 67 | return r0 68 | } 69 | 70 | // Run provides a mock function with given fields: _a0, _a1 71 | func (_m *Scanner) Run(_a0 number.Number, _a1 remote.ScannerOptions) (interface{}, error) { 72 | ret := _m.Called(_a0, _a1) 73 | 74 | if len(ret) == 0 { 75 | panic("no return value specified for Run") 76 | } 77 | 78 | var r0 interface{} 79 | var r1 error 80 | if rf, ok := ret.Get(0).(func(number.Number, remote.ScannerOptions) (interface{}, error)); ok { 81 | return rf(_a0, _a1) 82 | } 83 | if rf, ok := ret.Get(0).(func(number.Number, remote.ScannerOptions) interface{}); ok { 84 | r0 = rf(_a0, _a1) 85 | } else { 86 | if ret.Get(0) != nil { 87 | r0 = ret.Get(0).(interface{}) 88 | } 89 | } 90 | 91 | if rf, ok := ret.Get(1).(func(number.Number, remote.ScannerOptions) error); ok { 92 | r1 = rf(_a0, _a1) 93 | } else { 94 | r1 = ret.Error(1) 95 | } 96 | 97 | return r0, r1 98 | } 99 | 100 | // NewScanner creates a new instance of Scanner. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 101 | // The first argument is typically a *testing.T value. 102 | func NewScanner(t interface { 103 | mock.TestingT 104 | Cleanup(func()) 105 | }) *Scanner { 106 | mock := &Scanner{} 107 | mock.Mock.Test(t) 108 | 109 | t.Cleanup(func() { mock.AssertExpectations(t) }) 110 | 111 | return mock 112 | } 113 | -------------------------------------------------------------------------------- /support/docker/docker-compose.traefik.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | 3 | services: 4 | phoneinfoga: 5 | restart: on-failure 6 | image: sundowndev/phoneinfoga:latest 7 | command: 8 | - "serve" 9 | networks: 10 | - default 11 | - web 12 | environment: 13 | - GIN_MODE=release 14 | labels: 15 | - 'traefik.docker.network=web' 16 | - 'traefik.enable=true' 17 | - 'traefik.domain=demo.phoneinfoga.crvx.fr' 18 | - 'traefik.basic.frontend.rule=Host:demo.phoneinfoga.crvx.fr' 19 | - 'traefik.basic.port=5000' 20 | - 'traefik.basic.protocol=http' 21 | - 'traefik.frontend.headers.SSLRedirect=true' 22 | - 'traefik.frontend.headers.STSSeconds=315360000' 23 | - 'traefik.frontend.headers.browserXSSFilter=true' 24 | - 'traefik.frontend.headers.contentTypeNosniff=true' 25 | - 'traefik.frontend.headers.forceSTSHeader=true' 26 | - "traefik.frontend.headers.contentSecurityPolicy=default-src 'self';frame-ancestors 'self';style-src 'self';script-src 'self';img-src 'self';font-src 'self'" 27 | - 'traefik.frontend.headers.referrerPolicy=no-referrer' 28 | - 'traefik.frontend.headers.frameDeny=true' 29 | 30 | networks: 31 | web: 32 | external: true 33 | -------------------------------------------------------------------------------- /support/docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | 3 | services: 4 | phoneinfoga: 5 | container_name: phoneinfoga 6 | restart: on-failure 7 | build: 8 | context: ../.. 9 | dockerfile: ../../Dockerfile 10 | command: 11 | - "serve" 12 | ports: 13 | - "80:5000" 14 | environment: 15 | - GIN_MODE=release 16 | -------------------------------------------------------------------------------- /test/goldenfile/goldenfile.go: -------------------------------------------------------------------------------- 1 | package goldenfile 2 | 3 | import ( 4 | "flag" 5 | ) 6 | 7 | var Update = flag.String("update", "", "name of test to update") 8 | -------------------------------------------------------------------------------- /test/number.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import "github.com/sundowndev/phoneinfoga/v2/lib/number" 4 | 5 | func NewFakeUSNumber() *number.Number { 6 | n, _ := number.NewNumber("+1.4152229670") 7 | return n 8 | } 9 | -------------------------------------------------------------------------------- /web/client.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "embed" 5 | "fmt" 6 | "github.com/gin-gonic/gin" 7 | "io/ioutil" 8 | "mime" 9 | "net/http" 10 | "path" 11 | "strings" 12 | ) 13 | 14 | //go:embed client/dist/* 15 | var clientFS embed.FS 16 | 17 | const ( 18 | staticPath = "/" 19 | ) 20 | 21 | func detectContentType(path string, data []byte) string { 22 | arr := strings.Split(path, ".") 23 | ext := arr[len(arr)-1] 24 | 25 | if mimeType := mime.TypeByExtension(fmt.Sprintf(".%s", ext)); mimeType != "" { 26 | return mimeType 27 | } 28 | 29 | return http.DetectContentType(data) 30 | } 31 | 32 | func walkEmbededClient(dir string, router *gin.Engine) error { 33 | files, err := clientFS.ReadDir(dir) 34 | if err != nil { 35 | return err 36 | } 37 | for _, file := range files { 38 | if file.IsDir() { 39 | err := walkEmbededClient(path.Join(dir, file.Name()), router) 40 | if err != nil { 41 | return err 42 | } 43 | continue 44 | } 45 | 46 | assetPath := strings.ReplaceAll(path.Join(dir, file.Name()), "client/dist/", staticPath) 47 | f, err := clientFS.Open(path.Join(dir, file.Name())) 48 | if err != nil { 49 | return err 50 | } 51 | data, err := ioutil.ReadAll(f) 52 | if err != nil { 53 | return err 54 | } 55 | 56 | if assetPath == staticPath+"index.html" { 57 | assetPath = staticPath 58 | } 59 | 60 | router.GET(assetPath, func(c *gin.Context) { 61 | c.Header("Content-Type", detectContentType(assetPath, data)) 62 | c.Writer.WriteHeader(http.StatusOK) 63 | _, _ = c.Writer.Write(data) 64 | c.Abort() 65 | }) 66 | } 67 | return nil 68 | } 69 | 70 | func registerClientRoutes(router *gin.Engine) error { 71 | return walkEmbededClient("client/dist", router) 72 | } 73 | -------------------------------------------------------------------------------- /web/client/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | /tests/e2e/videos/ 6 | /tests/e2e/screenshots/ 7 | 8 | # local env files 9 | .env.local 10 | .env.*.local 11 | 12 | # Log files 13 | npm-debug.log* 14 | yarn-debug.log* 15 | yarn-error.log* 16 | 17 | # Editor directories and files 18 | .idea 19 | .vscode 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | .yarn/cache 26 | .yarn/*.gz 27 | !.yarn/releases/*.cjs 28 | -------------------------------------------------------------------------------- /web/client/.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | 3 | yarnPath: .yarn/releases/yarn-3.2.4.cjs 4 | -------------------------------------------------------------------------------- /web/client/README.md: -------------------------------------------------------------------------------- 1 | # client 2 | 3 | ## Project setup 4 | ``` 5 | yarn install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | yarn serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | yarn build 16 | ``` 17 | 18 | ### Run your unit tests 19 | ``` 20 | yarn test:unit 21 | ``` 22 | 23 | ### Run your end-to-end tests 24 | ``` 25 | yarn test:e2e 26 | ``` 27 | 28 | ### Lints and fixes files 29 | ``` 30 | yarn lint 31 | ``` 32 | 33 | ### Customize configuration 34 | See [Configuration Reference](https://cli.vuejs.org/config/). 35 | -------------------------------------------------------------------------------- /web/client/cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "pluginsFile": "tests/e2e/plugins/index.js" 3 | } 4 | -------------------------------------------------------------------------------- /web/client/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | transform: { 3 | "^.+\\.vuequot;: "vue-jest", 4 | ".+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)quot;: 5 | "jest-transform-stub", 6 | "^.+\\.tsx?quot;: "ts-jest", 7 | }, 8 | testMatch: [ 9 | "**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)", 10 | ], 11 | collectCoverage: true, 12 | collectCoverageFrom: [ 13 | "src/(store|config|components|views|utils|models)/**/*.(ts|vue)", 14 | ], 15 | }; 16 | -------------------------------------------------------------------------------- /web/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "build:watch": "vue-cli-service build --watch", 9 | "test": "yarn test:unit && yarn test:e2e", 10 | "test:unit": "vue-cli-service test:unit", 11 | "test:e2e": "vue-cli-service test:e2e", 12 | "lint": "vue-cli-service lint --no-fix", 13 | "lint:fix": "vue-cli-service lint --fix" 14 | }, 15 | "dependencies": { 16 | "axios": "0.21.1", 17 | "bootstrap": "4.6.0", 18 | "bootstrap-vue": "2.21.2", 19 | "jquery": "3.6.0", 20 | "popper.js": "1.16.1", 21 | "vue": "2.7.14", 22 | "vue-class-component": "7.2.6", 23 | "vue-json-viewer": "2.2.22", 24 | "vue-phone-number-input": "1.12.13", 25 | "vue-property-decorator": "9.1.2", 26 | "vue-router": "legacy", 27 | "vuex": "3.6.2" 28 | }, 29 | "devDependencies": { 30 | "@types/jest": "26.0.22", 31 | "@typescript-eslint/eslint-plugin": "5.61.0", 32 | "@typescript-eslint/parser": "5.61.0", 33 | "@vue/cli-plugin-e2e-cypress": "5.0.8", 34 | "@vue/cli-plugin-eslint": "5.0.8", 35 | "@vue/cli-plugin-router": "5.0.8", 36 | "@vue/cli-plugin-typescript": "5.0.8", 37 | "@vue/cli-plugin-unit-jest": "5.0.8", 38 | "@vue/cli-plugin-vuex": "5.0.8", 39 | "@vue/cli-service": "5.0.8", 40 | "@vue/eslint-config-prettier": "7.1.0", 41 | "@vue/eslint-config-typescript": "11.0.3", 42 | "@vue/test-utils": "1.3.6", 43 | "babel-core": "7.0.0-bridge.0", 44 | "eslint": "8.44.0", 45 | "eslint-plugin-prettier": "4.2.1", 46 | "eslint-plugin-vue": "9.15.1", 47 | "jest": "26.6.3", 48 | "node-gyp": "8.2.0", 49 | "prettier": "2.2.1", 50 | "ts-jest": "26.5.4", 51 | "typescript": "4.9.5", 52 | "vue-jest": "3.0.7", 53 | "vue-template-compiler": "2.6.12" 54 | }, 55 | "eslintConfig": { 56 | "root": true, 57 | "env": { 58 | "node": true 59 | }, 60 | "extends": [ 61 | "plugin:vue/essential", 62 | "eslint:recommended", 63 | "@vue/typescript/recommended", 64 | "@vue/prettier" 65 | ], 66 | "parserOptions": { 67 | "ecmaVersion": 2020 68 | }, 69 | "rules": { 70 | "vue/multi-word-component-names": "off" 71 | }, 72 | "overrides": [ 73 | { 74 | "files": [ 75 | "**/__tests__/*.{j,t}s?(x)", 76 | "**/tests/unit/**/*.spec.{j,t}s?(x)" 77 | ], 78 | "env": { 79 | "jest": true 80 | } 81 | } 82 | ] 83 | }, 84 | "browserslist": [ 85 | "> 1%", 86 | "last 2 versions" 87 | ], 88 | "packageManager": "yarn@3.2.4" 89 | } 90 | -------------------------------------------------------------------------------- /web/client/public/index.html: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | <html lang="en"> 3 | <head> 4 | <meta charset="utf-8"> 5 | <meta http-equiv="X-UA-Compatible" content="IE=edge"> 6 | <meta name="viewport" content="width=device-width,initial-scale=1.0"> 7 | <link rel="icon" href="<%= BASE_URL %>icon.svg"> 8 | <title>PhoneInfoga</title> 9 | <link type="text/css" rel="stylesheet" href="<%= BASE_URL %>css/bootstrap.min.css" /> 10 | <link type="text/css" rel="stylesheet" href="<%= BASE_URL %>css/bootstrap-vue.min.css" /> 11 | </head> 12 | <body> 13 | <noscript> 14 | <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong> 15 | </noscript> 16 | <div id="app"></div> 17 | <!-- built files will be auto injected --> 18 | </body> 19 | </html> 20 | -------------------------------------------------------------------------------- /web/client/src/App.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <div id="app" style="padding-bottom: 50px"> 3 | <div> 4 | <b-navbar toggleable="lg" type="dark" variant="dark"> 5 | <b-container> 6 | <b-navbar-brand to="/"> 7 | <img 8 | src="@/assets/logo.svg" 9 | class="d-inline-block align-top" 10 | width="30" 11 | height="30" 12 | alt="logo" 13 | /> 14 | {{ config.appName }} 15 | </b-navbar-brand> 16 | 17 | <b-collapse id="nav-text-collapse" is-nav> 18 | <b-navbar-nav> 19 | <b-nav-text>{{ config.appDescription }}</b-nav-text> 20 | </b-navbar-nav> 21 | </b-collapse> 22 | 23 | <b-navbar-nav class="ml-auto"> 24 | <b-collapse id="nav-collapse" is-nav> 25 | <b-navbar-nav> 26 | <b-nav-item 27 | href="https://github.com/sundowndev/phoneinfoga" 28 | target="_blank" 29 | >GitHub</b-nav-item 30 | > 31 | <b-nav-item 32 | href="https://sundowndev.github.io/phoneinfoga/resources/" 33 | target="_blank" 34 | >Resources</b-nav-item 35 | > 36 | <b-nav-item 37 | href="https://sundowndev.github.io/phoneinfoga/" 38 | target="_blank" 39 | >Documentation</b-nav-item 40 | > 41 | </b-navbar-nav> 42 | </b-collapse> 43 | </b-navbar-nav> 44 | </b-container> 45 | </b-navbar> 46 | </div> 47 | 48 | <b-container class="my-md-3"> 49 | <b-row> 50 | <b-col cols="12"> 51 | <b-alert v-if="isDemo" show variant="warning" fade 52 | >Welcome to the demo of PhoneInfoga web client.</b-alert 53 | > 54 | <b-alert 55 | v-for="(err, i) in errors" 56 | v-bind:key="i" 57 | show 58 | variant="danger" 59 | dismissible 60 | fade 61 | >{{ err.message }}</b-alert 62 | > 63 | 64 | <router-view /> 65 | </b-col> 66 | </b-row> 67 | </b-container> 68 | 69 | <b-navbar 70 | toggleable="lg" 71 | type="light" 72 | variant="light" 73 | fixed="bottom" 74 | v-if="version !== ''" 75 | > 76 | <b-container> 77 | <b-navbar-nav class="ml-auto"> 78 | <b-collapse id="nav-collapse" is-nav> 79 | <b-navbar-nav> 80 | <b-nav-item 81 | href="https://github.com/sundowndev/phoneinfoga/releases" 82 | target="_blank" 83 | >{{ config.appName }} {{ version }}</b-nav-item 84 | > 85 | </b-navbar-nav> 86 | </b-collapse> 87 | </b-navbar-nav> 88 | </b-container> 89 | </b-navbar> 90 | <script 91 | v-if="isDemo" 92 | type="application/javascript" 93 | defer 94 | data-domain="demo.phoneinfoga.crvx.fr" 95 | src="https://analytics.crvx.fr/js/script.js" 96 | ></script> 97 | </div> 98 | </template> 99 | 100 | <script lang="ts"> 101 | import Vue from "vue"; 102 | import { mapState } from "vuex"; 103 | import config from "@/config"; 104 | import axios, { AxiosResponse } from "axios"; 105 | 106 | type HealthResponse = { success: boolean; version: string; demo: boolean }; 107 | 108 | export default Vue.extend({ 109 | data: () => ({ config, version: "", isDemo: false }), 110 | computed: { 111 | ...mapState(["number", "errors"]), 112 | }, 113 | async created() { 114 | const res: AxiosResponse<HealthResponse> = await axios.get(config.apiUrl); 115 | 116 | this.version = res.data.version; 117 | this.isDemo = res.data.demo; 118 | }, 119 | }); 120 | </script> 121 | -------------------------------------------------------------------------------- /web/client/src/components/LocalScan.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <div v-if="loading || data.length > 0"> 3 | <h3>{{ name }} <b-spinner v-if="loading" type="grow"></b-spinner></h3> 4 | 5 | <b-table outlined :items="data" v-show="data.length > 0"></b-table> 6 | </div> 7 | </template> 8 | 9 | <script lang="ts"> 10 | import { Component, Vue, Prop } from "vue-property-decorator"; 11 | import axios from "axios"; 12 | import { mapState, mapMutations } from "vuex"; 13 | import config from "@/config"; 14 | import { ScanResponse } from "@/views/Scan.vue"; 15 | 16 | interface LocalScanResponse { 17 | number: string; 18 | dork: string; 19 | URL: string; 20 | } 21 | 22 | @Component 23 | export default class GoogleSearch extends Vue { 24 | id = "local"; 25 | name = "Local scan"; 26 | data: LocalScanResponse[] = []; 27 | loading = false; 28 | computed = { 29 | ...mapState(["number"]), 30 | ...mapMutations(["pushError"]), 31 | }; 32 | 33 | @Prop() scan!: Vue; 34 | 35 | mounted(): void { 36 | this.scan.$on("scan", async () => { 37 | this.loading = true; 38 | 39 | try { 40 | await this.run(); 41 | } catch (e) { 42 | this.$store.commit("pushError", { message: `${this.name}: ${e}` }); 43 | } 44 | 45 | this.loading = false; 46 | }); 47 | this.scan.$on("clear", this.clear); 48 | } 49 | 50 | private clear() { 51 | this.data = []; 52 | } 53 | 54 | private async run(): Promise<void> { 55 | const res: ScanResponse<LocalScanResponse> = await axios.get( 56 | `${config.apiUrl}/numbers/${this.$store.state.number}/scan/${this.id}`, 57 | { 58 | validateStatus: () => true, 59 | } 60 | ); 61 | 62 | if (!res.data.success && res.data.error) { 63 | throw res.data.error; 64 | } 65 | 66 | this.data.push(res.data.result); 67 | } 68 | } 69 | </script> 70 | -------------------------------------------------------------------------------- /web/client/src/components/NumverifyScan.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <div v-if="loading || data.length > 0"> 3 | <hr /> 4 | <h3>{{ name }} <b-spinner v-if="loading" type="grow"></b-spinner></h3> 5 | 6 | <b-button 7 | size="sm" 8 | variant="dark" 9 | v-b-toggle.numverify-collapse 10 | v-show="data.length > 0 && !loading" 11 | >Toggle results</b-button 12 | > 13 | <b-collapse id="numverify-collapse" class="mt-2"> 14 | <b-table 15 | outlined 16 | :stacked="data.length === 1" 17 | :items="data" 18 | v-show="data.length > 0" 19 | ></b-table> 20 | </b-collapse> 21 | </div> 22 | </template> 23 | 24 | <script lang="ts"> 25 | import { Component, Vue, Prop } from "vue-property-decorator"; 26 | import axios from "axios"; 27 | import { mapMutations } from "vuex"; 28 | import config from "@/config"; 29 | import { ScanResponse } from "@/views/Scan.vue"; 30 | 31 | interface NumverifyScanResponse { 32 | valid: boolean; 33 | number: string; 34 | localFormat: string; 35 | internationalFormat: string; 36 | countryPrefix: string; 37 | countryCode: string; 38 | countryName: string; 39 | location: string; 40 | carrier: string; 41 | lineType: string; 42 | } 43 | 44 | @Component 45 | export default class GoogleSearch extends Vue { 46 | id = "numverify"; 47 | name = "Numverify scan"; 48 | data: NumverifyScanResponse[] = []; 49 | loading = false; 50 | computed = { 51 | ...mapMutations(["pushError"]), 52 | }; 53 | 54 | @Prop() scan!: Vue; 55 | 56 | mounted(): void { 57 | this.scan.$on("scan", async () => { 58 | this.loading = true; 59 | 60 | try { 61 | await this.run(); 62 | } catch (e) { 63 | this.$store.commit("pushError", { message: `${this.name}: ${e}` }); 64 | } 65 | 66 | this.loading = false; 67 | this.scan.$emit("finished"); 68 | }); 69 | this.scan.$on("clear", this.clear); 70 | } 71 | 72 | private clear() { 73 | this.data = []; 74 | } 75 | 76 | private async run(): Promise<void> { 77 | const res: ScanResponse<NumverifyScanResponse> = await axios.get( 78 | `${config.apiUrl}/numbers/${this.$store.state.number}/scan/${this.id}`, 79 | { 80 | validateStatus: () => true, 81 | } 82 | ); 83 | 84 | if (!res.data.success && res.data.error) { 85 | throw res.data.error; 86 | } 87 | 88 | this.data.push(res.data.result); 89 | } 90 | } 91 | </script> 92 | -------------------------------------------------------------------------------- /web/client/src/components/OVHScan.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <div v-if="loading || data.length > 0" class="mt-2"> 3 | <hr /> 4 | <h3> 5 | {{ name }} 6 | <b-spinner v-if="loading" type="grow"></b-spinner> 7 | </h3> 8 | 9 | <b-button 10 | size="sm" 11 | variant="dark" 12 | v-b-toggle.ovh-collapse 13 | v-show="data.length > 0 && !loading" 14 | >Toggle results 15 | </b-button> 16 | <b-collapse id="ovh-collapse" class="mt-2"> 17 | <b-table 18 | outlined 19 | :stacked="data.length === 1" 20 | :items="data" 21 | v-show="data.length > 0" 22 | ></b-table> 23 | </b-collapse> 24 | 25 | <hr /> 26 | </div> 27 | </template> 28 | 29 | <script lang="ts"> 30 | import { Component, Prop, Vue } from "vue-property-decorator"; 31 | import axios from "axios"; 32 | import { mapMutations } from "vuex"; 33 | import config from "@/config"; 34 | import { ScanResponse } from "@/views/Scan.vue"; 35 | 36 | interface OVHScanResponse { 37 | found: boolean; 38 | numberRange: string; 39 | city: string; 40 | zipCode: string; 41 | } 42 | 43 | @Component 44 | export default class GoogleSearch extends Vue { 45 | id = "ovh"; 46 | name = "OVH Telecom scan"; 47 | data: OVHScanResponse[] = []; 48 | loading = false; 49 | computed = { 50 | ...mapMutations(["pushError"]), 51 | }; 52 | 53 | @Prop() scan!: Vue; 54 | 55 | mounted(): void { 56 | this.scan.$on("scan", async () => { 57 | this.loading = true; 58 | 59 | try { 60 | await this.run(); 61 | } catch (e) { 62 | this.$store.commit("pushError", { message: `${this.name}: ${e}` }); 63 | } 64 | 65 | this.loading = false; 66 | }); 67 | this.scan.$on("clear", this.clear); 68 | } 69 | 70 | private clear() { 71 | this.data = []; 72 | } 73 | 74 | private async run(): Promise<void> { 75 | const res: ScanResponse<OVHScanResponse> = await axios.get( 76 | `${config.apiUrl}/numbers/${this.$store.state.number}/scan/${this.id}`, 77 | { 78 | validateStatus: () => true, 79 | } 80 | ); 81 | 82 | if (!res.data.success && res.data.error) { 83 | throw res.data.error; 84 | } 85 | 86 | this.data.push(res.data.result); 87 | } 88 | } 89 | </script> 90 | -------------------------------------------------------------------------------- /web/client/src/components/Scanner.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <b-container class="mb-3"> 3 | <b-row align-h="between" align-v="center"> 4 | <h3>{{ name }}</h3> 5 | <b-button 6 | v-if="!error && !loading && !data" 7 | @click="runScan" 8 | variant="dark" 9 | size="lg" 10 | >Run</b-button 11 | > 12 | <b-spinner v-if="loading && !error" type="grow"></b-spinner> 13 | <b-row v-if="error && !loading"> 14 | <b-alert class="m-0" show variant="danger" fade>{{ error }}</b-alert> 15 | <b-button 16 | v-if="!dryrunError" 17 | @click="runScan" 18 | variant="danger" 19 | size="lg" 20 | >Retry</b-button 21 | > 22 | </b-row> 23 | </b-row> 24 | <b-collapse :id="collapseId" class="mt-2 text-left"> 25 | <JsonViewer :value="data"></JsonViewer> 26 | </b-collapse> 27 | </b-container> 28 | </template> 29 | 30 | <script lang="ts"> 31 | import { Component, Vue, Prop } from "vue-property-decorator"; 32 | import axios from "axios"; 33 | import { mapState, mapMutations } from "vuex"; 34 | import JsonViewer from "vue-json-viewer"; 35 | import config from "@/config"; 36 | 37 | @Component({ 38 | components: { 39 | JsonViewer, 40 | }, 41 | }) 42 | export default class Scanner extends Vue { 43 | data = null; 44 | loading = false; 45 | dryrunError = false; 46 | error: unknown = null; 47 | computed = { 48 | ...mapState(["number"]), 49 | ...mapMutations(["pushError"]), 50 | }; 51 | 52 | @Prop() scanId!: string; 53 | @Prop() name!: string; 54 | 55 | collapseId = "scanner-collapse" + this.scanId; 56 | 57 | mounted(): void { 58 | this.dryRun(); 59 | } 60 | 61 | private async dryRun(): Promise<void> { 62 | try { 63 | const res = await axios.post( 64 | `${config.apiUrl}/v2/scanners/${this.scanId}/dryrun`, 65 | { 66 | number: this.$store.state.number, 67 | }, 68 | { 69 | validateStatus: () => true, 70 | } 71 | ); 72 | 73 | if (!res.data.success && res.data.error) { 74 | throw res.data.error; 75 | } 76 | } catch (error: unknown) { 77 | this.dryrunError = true; 78 | this.error = error; 79 | } 80 | } 81 | 82 | private async runScan(): Promise<void> { 83 | this.error = null; 84 | this.loading = true; 85 | try { 86 | const res = await axios.post( 87 | `${config.apiUrl}/v2/scanners/${this.scanId}/run`, 88 | { 89 | number: this.$store.state.number, 90 | }, 91 | { 92 | validateStatus: () => true, 93 | } 94 | ); 95 | 96 | if (!res.data.success && res.data.error) { 97 | throw res.data.error; 98 | } 99 | this.data = res.data.result; 100 | this.$root.$emit("bv::toggle::collapse", this.collapseId); 101 | } catch (error) { 102 | this.error = error; 103 | } 104 | 105 | this.loading = false; 106 | } 107 | } 108 | </script> 109 | -------------------------------------------------------------------------------- /web/client/src/config/index.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | appName: "PhoneInfoga", 3 | appDescription: 4 | "Advanced information gathering & OSINT tool for phone numbers", 5 | apiUrl: "/api", 6 | }; 7 | -------------------------------------------------------------------------------- /web/client/src/main.ts: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import { BootstrapVue, IconsPlugin } from "bootstrap-vue"; 3 | 4 | import App from "./App.vue"; 5 | import router from "./router"; 6 | import store from "./store"; 7 | 8 | Vue.config.productionTip = false; 9 | 10 | // Install BootstrapVue 11 | Vue.use(BootstrapVue); 12 | // Optionally install the BootstrapVue icon components plugin 13 | Vue.use(IconsPlugin); 14 | 15 | new Vue({ 16 | router, 17 | store, 18 | render: (h) => h(App), 19 | }).$mount("#app"); 20 | -------------------------------------------------------------------------------- /web/client/src/router/index.ts: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import VueRouter from "vue-router"; 3 | import Scan from "../views/Scan.vue"; 4 | import Number from "../views/Number.vue"; 5 | import NotFound from "../views/NotFound.vue"; 6 | 7 | Vue.use(VueRouter); 8 | 9 | const routes = [ 10 | { 11 | path: "/", 12 | name: "Scan", 13 | component: Scan, 14 | }, 15 | { 16 | path: "/numbers/:number", 17 | name: "Number", 18 | component: Number, 19 | }, 20 | { 21 | path: "*", 22 | name: "NotFound", 23 | component: NotFound, 24 | }, 25 | ]; 26 | 27 | const router = new VueRouter({ 28 | mode: "hash", 29 | base: process.env.BASE_URL, 30 | routes, 31 | }); 32 | 33 | export default router; 34 | -------------------------------------------------------------------------------- /web/client/src/shims-tsx.d.ts: -------------------------------------------------------------------------------- 1 | import Vue, { VNode } from "vue"; 2 | 3 | declare global { 4 | namespace JSX { 5 | // tslint:disable no-empty-interface 6 | interface Element extends VNode {} 7 | // tslint:disable no-empty-interface 8 | interface ElementClass extends Vue {} 9 | interface IntrinsicElements { 10 | [elem: string]: any; 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /web/client/src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.vue" { 2 | import Vue from "vue"; 3 | export default Vue; 4 | } 5 | declare module "vue-json-viewer" {} 6 | declare module "vue-phone-number-input" {} 7 | -------------------------------------------------------------------------------- /web/client/src/store/index.ts: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import Vuex, { Store } from "vuex"; 3 | 4 | Vue.use(Vuex); 5 | 6 | interface ErrorAlert { 7 | message: string; 8 | } 9 | 10 | interface StoreInterface { 11 | number: string; 12 | errors: ErrorAlert[]; 13 | } 14 | 15 | const store: Store<StoreInterface> = new Vuex.Store({ 16 | state: { 17 | number: "", 18 | errors: [] as ErrorAlert[], 19 | }, 20 | mutations: { 21 | pushError(state, error: ErrorAlert): void { 22 | state.errors.push(error); 23 | }, 24 | setNumber(state, number: string): void { 25 | state.number = number; 26 | }, 27 | resetState(state) { 28 | state.number = ""; 29 | state.errors = []; 30 | 31 | return state; 32 | }, 33 | }, 34 | getters: {}, 35 | actions: { 36 | resetState(context): void { 37 | context.commit("resetState"); 38 | }, 39 | }, 40 | modules: {}, 41 | }); 42 | 43 | export default store; 44 | -------------------------------------------------------------------------------- /web/client/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import config from "../config/index"; 3 | interface ScannerObject { 4 | name: string; 5 | description: string; 6 | } 7 | 8 | const formatNumber = (number: string): string => { 9 | return number.replace(/[_\W]+/g, ""); 10 | }; 11 | 12 | const isValid = (number: string): boolean => { 13 | const formatted = formatNumber(number); 14 | 15 | return formatted.match(/^[0-9]+$/) !== null && formatted.length > 2; 16 | }; 17 | 18 | const formatString = (string: string): string => { 19 | return string.replace(/([A-Z])/g, " $1").trim(); 20 | }; 21 | 22 | const getScanners = async (): Promise<ScannerObject[]> => { 23 | const res = await axios.get(`${config.apiUrl}/v2/scanners`); 24 | 25 | // TODO: Remove this filter once the scanner local is remove 26 | return res.data.scanners.filter( 27 | (scanner: ScannerObject) => scanner.name !== "local" 28 | ); 29 | }; 30 | 31 | export { formatNumber, isValid, formatString, getScanners }; 32 | -------------------------------------------------------------------------------- /web/client/src/views/NotFound.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <div> 3 | <h1>Page not found</h1> 4 | </div> 5 | </template> 6 | 7 | <script> 8 | export default { 9 | name: "NotFound", 10 | }; 11 | </script> 12 | -------------------------------------------------------------------------------- /web/client/src/views/Number.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <div> 3 | <b-card 4 | v-if="isLookup || showInformations" 5 | header="Informations" 6 | class="mb-3 mt-3 text-center" 7 | > 8 | <b-list-group flush> 9 | <b-list-group-item 10 | v-for="(value, name) in localData" 11 | :key="name" 12 | class="text-left d-flex" 13 | > 14 | <h5 class="text-capitalize m-0 mr-4">{{ formatString(name) }}:</h5> 15 | <p class="m-0">{{ value }}</p> 16 | </b-list-group-item> 17 | </b-list-group> 18 | </b-card> 19 | 20 | <b-card v-if="isLookup" header="Scanners" class="text-center"> 21 | <Scanner 22 | v-for="(scanner, index) in scanners" 23 | :key="index" 24 | :name="scanner.name.charAt(0).toUpperCase() + scanner.name.slice(1)" 25 | :scanId="scanner.name" 26 | /> 27 | </b-card> 28 | </div> 29 | </template> 30 | 31 | <script lang="ts"> 32 | import Vue from "vue"; 33 | import { mapMutations, mapState } from "vuex"; 34 | import { formatNumber, isValid, formatString, getScanners } from "../utils"; 35 | import Scanner from "../components/Scanner.vue"; 36 | import axios, { AxiosResponse } from "axios"; 37 | import config from "@/config"; 38 | 39 | interface ScannerObject { 40 | name: string; 41 | description: string; 42 | } 43 | 44 | interface Data { 45 | loading: boolean; 46 | isLookup: boolean; 47 | showInformations: boolean; 48 | scanners: Array<ScannerObject>; 49 | localData: { 50 | valid: boolean; 51 | raw_local: string; 52 | local: string; 53 | e164: string; 54 | international: string; 55 | countryCode: number; 56 | country: string; 57 | carrier: string; 58 | }; 59 | } 60 | 61 | export type ScanResponse<T> = AxiosResponse<{ 62 | success: boolean; 63 | result: T; 64 | error: string; 65 | }>; 66 | 67 | export default Vue.extend({ 68 | components: { Scanner }, 69 | computed: { 70 | ...mapState(["number"]), 71 | ...mapMutations(["pushError"]), 72 | }, 73 | data(): Data { 74 | return { 75 | loading: false, 76 | isLookup: false, 77 | showInformations: false, 78 | scanners: [], 79 | localData: { 80 | valid: false, 81 | raw_local: "", 82 | local: "", 83 | e164: "", 84 | international: "", 85 | countryCode: 33, 86 | country: "", 87 | carrier: "", 88 | }, 89 | }; 90 | }, 91 | mounted() { 92 | this.runScans(); 93 | }, 94 | methods: { 95 | formatString: formatString, 96 | async getScanners() { 97 | try { 98 | this.scanners = await getScanners(); 99 | } catch (error) { 100 | this.$store.commit("pushError", { message: error }); 101 | } 102 | }, 103 | async runScans(): Promise<void> { 104 | if (!isValid(this.$route.params.number)) { 105 | this.$store.commit("pushError", { message: "Number is not valid." }); 106 | return; 107 | } 108 | 109 | this.loading = true; 110 | 111 | this.$store.commit("setNumber", formatNumber(this.$route.params.number)); 112 | 113 | try { 114 | const res = await axios.post(`${config.apiUrl}/v2/numbers`, { 115 | number: this.$store.state.number, 116 | }); 117 | 118 | this.localData = res.data; 119 | 120 | if (this.localData.valid) { 121 | this.getScanners(); 122 | this.isLookup = true; 123 | } else { 124 | this.showInformations = true; 125 | } 126 | } catch (error) { 127 | this.$store.commit("pushError", { message: error }); 128 | } 129 | 130 | this.loading = false; 131 | }, 132 | }, 133 | }); 134 | </script> 135 | -------------------------------------------------------------------------------- /web/client/src/views/Scan.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <div> 3 | <b-form @submit="onSubmit" class="d-flex justify-content-center mt-5"> 4 | <b-form-group id="input-group-1" label-for="input-1"> 5 | <b-input-group> 6 | <VuePhoneNumberInput 7 | v-model="inputNumberVal" 8 | :disabled="loading" 9 | @update="updateInputNumber" 10 | /> 11 | <b-button 12 | size="sm" 13 | variant="dark" 14 | v-on:click="runScans" 15 | :disabled="loading" 16 | class="mr-2 ml-2" 17 | > 18 | <b-icon-play-fill></b-icon-play-fill> 19 | Lookup 20 | </b-button> 21 | 22 | <b-button 23 | variant="danger" 24 | size="sm" 25 | v-on:click="clearData" 26 | v-show="number" 27 | :disabled="loading" 28 | >Reset 29 | </b-button> 30 | </b-input-group> 31 | </b-form-group> 32 | </b-form> 33 | 34 | <b-card 35 | v-if="isLookup || showInformations" 36 | header="Informations" 37 | class="mb-3 mt-3 text-center" 38 | > 39 | <b-list-group flush> 40 | <b-list-group-item 41 | v-for="(value, name) in localData" 42 | :key="name" 43 | class="text-left d-flex" 44 | > 45 | <h5 class="text-capitalize m-0 mr-4">{{ formatString(name) }}:</h5> 46 | <p class="m-0">{{ value }}</p> 47 | </b-list-group-item> 48 | </b-list-group> 49 | </b-card> 50 | 51 | <b-card v-if="isLookup" header="Scanners" class="text-center"> 52 | <Scanner 53 | v-for="(scanner, index) in scanners" 54 | :key="index" 55 | :name="scanner.name.charAt(0).toUpperCase() + scanner.name.slice(1)" 56 | :scanId="scanner.name" 57 | /> 58 | </b-card> 59 | </div> 60 | </template> 61 | 62 | <script lang="ts"> 63 | import Vue from "vue"; 64 | import { mapMutations, mapState } from "vuex"; 65 | import { formatNumber, isValid, formatString, getScanners } from "../utils"; 66 | import VuePhoneNumberInput from "vue-phone-number-input"; 67 | import Scanner from "../components/Scanner.vue"; 68 | import axios, { AxiosResponse } from "axios"; 69 | import config from "@/config"; 70 | 71 | interface InputNumberObject { 72 | countryCallingCode: string; 73 | countryCode: string; 74 | e164: string; 75 | formatInternational: string; 76 | formatNational: string; 77 | formattedNumber: string; 78 | isValid: boolean; 79 | nationalNumber: string; 80 | phoneNumber: string; 81 | type: string; 82 | uri: string; 83 | } 84 | 85 | interface ScannerObject { 86 | name: string; 87 | description: string; 88 | } 89 | 90 | interface Data { 91 | loading: boolean; 92 | isLookup: boolean; 93 | showInformations: boolean; 94 | inputNumber: string; 95 | inputNumberVal: string; 96 | scanEvent: Vue; 97 | scanners: ScannerObject[]; 98 | localData: { 99 | valid: boolean; 100 | raw_local: string; 101 | local: string; 102 | e164: string; 103 | international: string; 104 | countryCode: number; 105 | country: string; 106 | carrier: string; 107 | }; 108 | } 109 | 110 | export type ScanResponse<T> = AxiosResponse<{ 111 | success: boolean; 112 | result: T; 113 | error: string; 114 | }>; 115 | 116 | export default Vue.extend({ 117 | components: { Scanner, VuePhoneNumberInput }, 118 | computed: { 119 | ...mapState(["number"]), 120 | ...mapMutations(["pushError"]), 121 | }, 122 | data(): Data { 123 | return { 124 | loading: false, 125 | isLookup: false, 126 | showInformations: false, 127 | inputNumber: "", 128 | inputNumberVal: "", 129 | scanEvent: new Vue(), 130 | scanners: [], 131 | localData: { 132 | valid: false, 133 | raw_local: "", 134 | local: "", 135 | e164: "", 136 | international: "", 137 | countryCode: 33, 138 | country: "", 139 | carrier: "", 140 | }, 141 | }; 142 | }, 143 | methods: { 144 | formatString: formatString, 145 | clearData() { 146 | this.isLookup = false; 147 | this.showInformations = false; 148 | this.$store.commit("resetState"); 149 | }, 150 | async runScans(): Promise<void> { 151 | this.clearData(); 152 | if (!isValid(this.inputNumber)) { 153 | this.$store.commit("pushError", { message: "Number is not valid." }); 154 | return; 155 | } 156 | 157 | this.loading = true; 158 | 159 | this.$store.commit("setNumber", formatNumber(this.inputNumber)); 160 | 161 | try { 162 | const res = await axios.post(`${config.apiUrl}/v2/numbers`, { 163 | number: this.$store.state.number, 164 | }); 165 | 166 | this.localData = res.data; 167 | 168 | if (this.localData.valid) { 169 | this.getScanners(); 170 | this.isLookup = true; 171 | } else { 172 | this.showInformations = true; 173 | } 174 | } catch (error) { 175 | this.$store.commit("pushError", { message: error }); 176 | } 177 | 178 | this.loading = false; 179 | }, 180 | onSubmit(evt: Event) { 181 | evt.preventDefault(); 182 | }, 183 | updateInputNumber(val: InputNumberObject) { 184 | this.inputNumber = val.e164; 185 | }, 186 | async getScanners() { 187 | try { 188 | this.scanners = await getScanners(); 189 | } catch (error) { 190 | this.$store.commit("pushError", { message: error }); 191 | } 192 | }, 193 | }, 194 | }); 195 | </script> 196 | 197 | <style src="vue-phone-number-input/dist/vue-phone-number-input.css"></style> 198 | -------------------------------------------------------------------------------- /web/client/tests/e2e/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: ["cypress"], 3 | env: { 4 | mocha: true, 5 | "cypress/globals": true, 6 | }, 7 | rules: { 8 | strict: "off", 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /web/client/tests/e2e/plugins/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable arrow-body-style */ 2 | // https://docs.cypress.io/guides/guides/plugins-guide.html 3 | 4 | // if you need a custom webpack configuration you can uncomment the following import 5 | // and then use the `file:preprocessor` event 6 | // as explained in the cypress docs 7 | // https://docs.cypress.io/api/plugins/preprocessors-api.html#Examples 8 | 9 | // /* eslint-disable import/no-extraneous-dependencies, global-require */ 10 | // const webpack = require('@cypress/webpack-preprocessor') 11 | 12 | module.exports = (on, config) => { 13 | // on('file:preprocessor', webpack({ 14 | // webpackOptions: require('@vue/cli-service/webpack.config'), 15 | // watchOptions: {} 16 | // })) 17 | 18 | return Object.assign({}, config, { 19 | fixturesFolder: "tests/e2e/fixtures", 20 | integrationFolder: "tests/e2e/specs", 21 | screenshotsFolder: "tests/e2e/screenshots", 22 | videosFolder: "tests/e2e/videos", 23 | supportFile: "tests/e2e/support/index.js", 24 | }); 25 | }; 26 | -------------------------------------------------------------------------------- /web/client/tests/e2e/specs/test.js: -------------------------------------------------------------------------------- 1 | // https://docs.cypress.io/api/introduction/api.html 2 | 3 | describe("My First Test", () => { 4 | it("Visits the app root url", () => { 5 | cy.visit("/"); 6 | cy.contains("h1", "Welcome to Your Vue.js + TypeScript App"); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /web/client/tests/e2e/support/commands.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add("login", (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This is will overwrite an existing command -- 25 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) 26 | -------------------------------------------------------------------------------- /web/client/tests/e2e/support/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import "./commands"; 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | -------------------------------------------------------------------------------- /web/client/tests/unit/config.spec.ts: -------------------------------------------------------------------------------- 1 | // import { shallowMount } from "@vue/test-utils"; 2 | // import HelloWorld from "@/components/HelloWorld.vue"; 3 | import config from "../../src/config"; 4 | 5 | describe("HelloWorld.vue", () => { 6 | it("renders props.msg when passed", () => { 7 | // const msg = "new message"; 8 | // const wrapper = shallowMount(HelloWorld, { 9 | // propsData: { msg } 10 | // }); 11 | expect(config.appName).toBe("PhoneInfoga"); 12 | expect(config.appDescription).toBe( 13 | "Advanced information gathering & OSINT tool for phone numbers" 14 | ); 15 | expect(config.apiUrl).toBe("/api"); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /web/client/tests/unit/utils.spec.ts: -------------------------------------------------------------------------------- 1 | import { formatNumber, isValid } from "../../src/utils"; 2 | 3 | describe("src/utils", () => { 4 | describe("#formatNumber", () => { 5 | it("should format phone number", () => { 6 | expect(formatNumber("+1 (555) 444-3333")).toBe("15554443333"); 7 | expect(formatNumber("1/(555)_444-3333")).toBe("15554443333"); 8 | }); 9 | }); 10 | 11 | describe("#isValid", () => { 12 | it("should check if number is valid", () => { 13 | expect(isValid("+1/(555)_444-3333")).toBe(true); 14 | expect(isValid("1 555 4443333")).toBe(true); 15 | expect(isValid("this1 555 4443333")).toBe(false); 16 | }); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /web/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "esnext", 5 | "strict": true, 6 | "jsx": "preserve", 7 | "importHelpers": true, 8 | "moduleResolution": "node", 9 | "experimentalDecorators": true, 10 | "esModuleInterop": true, 11 | "allowSyntheticDefaultImports": true, 12 | "sourceMap": true, 13 | "baseUrl": ".", 14 | "types": [ 15 | "webpack-env", 16 | "jest" 17 | ], 18 | "paths": { 19 | "@/*": [ 20 | "src/*" 21 | ] 22 | }, 23 | "lib": [ 24 | "esnext", 25 | "dom", 26 | "dom.iterable", 27 | "scripthost" 28 | ] 29 | }, 30 | "include": [ 31 | "src/**/*.ts", 32 | "src/**/*.tsx", 33 | "src/**/*.vue", 34 | "tests/**/*.ts", 35 | "tests/**/*.tsx" 36 | ], 37 | "exclude": [ 38 | "node_modules" 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /web/client/vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | publicPath: "/", 3 | }; 4 | -------------------------------------------------------------------------------- /web/controllers.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/sundowndev/phoneinfoga/v2/build" 6 | "github.com/sundowndev/phoneinfoga/v2/lib/number" 7 | "github.com/sundowndev/phoneinfoga/v2/lib/remote" 8 | "github.com/sundowndev/phoneinfoga/v2/lib/remote/suppliers" 9 | "github.com/sundowndev/phoneinfoga/v2/web/errors" 10 | "net/http" 11 | ) 12 | 13 | type ScanResultResponse struct { 14 | JSONResponse 15 | Result interface{} `json:"result"` 16 | } 17 | 18 | type getAllNumbersResponse struct { 19 | JSONResponse 20 | Numbers []number.Number `json:"numbers"` 21 | } 22 | 23 | type healthResponse struct { 24 | Success bool `json:"success"` 25 | Version string `json:"version"` 26 | Commit string `json:"commit"` 27 | Demo bool `json:"demo"` 28 | } 29 | 30 | // @ID getAllNumbers 31 | // @Tags Numbers 32 | // @Summary Fetch all previously scanned numbers. 33 | // @Description This route is actually not used yet. 34 | // @Deprecated 35 | // @Produce json 36 | // @Success 200 {object} getAllNumbersResponse 37 | // @Router /numbers [get] 38 | func getAllNumbers(c *gin.Context) { 39 | c.JSON(http.StatusOK, getAllNumbersResponse{ 40 | JSONResponse: JSONResponse{Success: true}, 41 | Numbers: []number.Number{}, 42 | }) 43 | } 44 | 45 | // @ID validate 46 | // @Tags Numbers 47 | // @Summary Check if a number is valid and possible. 48 | // @Produce json 49 | // @Deprecated 50 | // @Success 200 {object} JSONResponse 51 | // @Success 400 {object} JSONResponse 52 | // @Router /numbers/{number}/validate [get] 53 | // @Param number path string true "Input phone number" validate(required) 54 | func validate(c *gin.Context) { 55 | _, err := number.NewNumber(c.Param("number")) 56 | if err != nil { 57 | handleError(c, errors.NewBadRequest(err)) 58 | return 59 | } 60 | c.JSON(http.StatusOK, successResponse("The number is valid")) 61 | } 62 | 63 | // @ID localScan 64 | // @Tags Numbers 65 | // @Summary Perform a scan using local phone number library. 66 | // @Produce json 67 | // @Deprecated 68 | // @Success 200 {object} ScanResultResponse{result=number.Number} 69 | // @Success 400 {object} JSONResponse 70 | // @Router /numbers/{number}/scan/local [get] 71 | // @Param number path string true "Input phone number" validate(required) 72 | func localScan(c *gin.Context) { 73 | num, err := number.NewNumber(c.Param("number")) 74 | if err != nil { 75 | handleError(c, errors.NewBadRequest(err)) 76 | return 77 | } 78 | 79 | result, err := remote.NewLocalScanner().Run(*num, make(remote.ScannerOptions)) 80 | if err != nil { 81 | handleError(c, errors.NewInternalError(err)) 82 | return 83 | } 84 | 85 | c.JSON(http.StatusOK, ScanResultResponse{ 86 | JSONResponse: JSONResponse{Success: true}, 87 | Result: result, 88 | }) 89 | } 90 | 91 | // @ID numverifyScan 92 | // @Tags Numbers 93 | // @Summary Perform a scan using Numverify's API. 94 | // @Deprecated 95 | // @Produce json 96 | // @Success 200 {object} ScanResultResponse{result=remote.NumverifyScannerResponse} 97 | // @Success 400 {object} JSONResponse 98 | // @Router /numbers/{number}/scan/numverify [get] 99 | // @Param number path string true "Input phone number" validate(required) 100 | func numverifyScan(c *gin.Context) { 101 | num, err := number.NewNumber(c.Param("number")) 102 | if err != nil { 103 | handleError(c, errors.NewBadRequest(err)) 104 | return 105 | } 106 | 107 | result, err := remote.NewNumverifyScanner(suppliers.NewNumverifySupplier()).Run(*num, make(remote.ScannerOptions)) 108 | if err != nil { 109 | handleError(c, errors.NewInternalError(err)) 110 | return 111 | } 112 | 113 | c.JSON(http.StatusOK, ScanResultResponse{ 114 | JSONResponse: JSONResponse{Success: true}, 115 | Result: result, 116 | }) 117 | } 118 | 119 | // @ID googleSearchScan 120 | // @Tags Numbers 121 | // @Summary Perform a scan using Google Search engine. 122 | // @Deprecated 123 | // @Produce json 124 | // @Success 200 {object} ScanResultResponse{result=remote.GoogleSearchResponse} 125 | // @Success 400 {object} JSONResponse 126 | // @Router /numbers/{number}/scan/googlesearch [get] 127 | // @Param number path string true "Input phone number" validate(required) 128 | func googleSearchScan(c *gin.Context) { 129 | num, err := number.NewNumber(c.Param("number")) 130 | if err != nil { 131 | handleError(c, errors.NewBadRequest(err)) 132 | return 133 | } 134 | 135 | result, err := remote.NewGoogleSearchScanner().Run(*num, make(remote.ScannerOptions)) 136 | if err != nil { 137 | handleError(c, errors.NewInternalError(err)) 138 | return 139 | } 140 | 141 | c.JSON(http.StatusOK, ScanResultResponse{ 142 | JSONResponse: JSONResponse{Success: true}, 143 | Result: result, 144 | }) 145 | } 146 | 147 | // @ID ovhScan 148 | // @Tags Numbers 149 | // @Summary Perform a scan using OVH's API. 150 | // @Deprecated 151 | // @Produce json 152 | // @Success 200 {object} ScanResultResponse{result=remote.OVHScannerResponse} 153 | // @Success 400 {object} JSONResponse 154 | // @Router /numbers/{number}/scan/ovh [get] 155 | // @Param number path string true "Input phone number" validate(required) 156 | func ovhScan(c *gin.Context) { 157 | num, err := number.NewNumber(c.Param("number")) 158 | if err != nil { 159 | handleError(c, errors.NewBadRequest(err)) 160 | return 161 | } 162 | 163 | result, err := remote.NewOVHScanner(suppliers.NewOVHSupplier()).Run(*num, make(remote.ScannerOptions)) 164 | if err != nil { 165 | handleError(c, errors.NewInternalError(err)) 166 | return 167 | } 168 | 169 | c.JSON(http.StatusOK, ScanResultResponse{ 170 | JSONResponse: JSONResponse{Success: true}, 171 | Result: result, 172 | }) 173 | } 174 | 175 | // @ID healthCheck 176 | // @Tags General 177 | // @Summary Check if service is healthy. 178 | // @Produce json 179 | // @Success 200 {object} healthResponse 180 | // @Success 500 {object} JSONResponse 181 | // @Router / [get] 182 | func healthHandler(c *gin.Context) { 183 | c.JSON(http.StatusOK, healthResponse{ 184 | Success: true, 185 | Version: build.Version, 186 | Commit: build.Commit, 187 | Demo: build.IsDemo(), 188 | }) 189 | } 190 | -------------------------------------------------------------------------------- /web/errors.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/sundowndev/phoneinfoga/v2/web/errors" 6 | ) 7 | 8 | func handleError(c *gin.Context, e *errors.Error) { 9 | c.JSON(e.Status(), JSONResponse{Success: false, Error: e.String()}) 10 | c.Abort() 11 | } 12 | -------------------------------------------------------------------------------- /web/errors/errors.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | ) 7 | 8 | type Error struct { 9 | status int 10 | err error 11 | } 12 | 13 | func (e *Error) Status() int { 14 | return e.status 15 | } 16 | 17 | func (e *Error) Error() error { 18 | return e.err 19 | } 20 | 21 | func (e *Error) String() string { 22 | if e.err == nil { 23 | return "unknown error" 24 | } 25 | return e.err.Error() 26 | } 27 | 28 | func NewBadRequest(err error) *Error { 29 | if err == nil { 30 | err = errors.New("bad request") 31 | } 32 | return &Error{ 33 | status: http.StatusBadRequest, 34 | err: err, 35 | } 36 | } 37 | 38 | func NewInternalError(err error) *Error { 39 | if err == nil { 40 | err = errors.New("internal error") 41 | } 42 | return &Error{ 43 | status: http.StatusInternalServerError, 44 | err: err, 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /web/response.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | func successResponse(msg ...string) JSONResponse { 8 | var message string = "" 9 | 10 | if len(msg) > 0 { 11 | message = strings.Join(msg, " ") 12 | } 13 | 14 | return JSONResponse{ 15 | Success: true, 16 | Message: message, 17 | } 18 | } 19 | 20 | func errorResponse(msg ...string) JSONResponse { 21 | var message string = "An error occurred" 22 | 23 | if len(msg) > 0 { 24 | message = strings.Join(msg, " ") 25 | } 26 | 27 | return JSONResponse{ 28 | Success: false, 29 | Error: message, 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /web/response_test.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "testing" 5 | 6 | assertion "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestResponse(t *testing.T) { 10 | assert := assertion.New(t) 11 | 12 | t.Run("successResponse", func(t *testing.T) { 13 | t.Run("should return success response", func(t *testing.T) { 14 | result := successResponse() 15 | 16 | assert.IsType(JSONResponse{}, result) 17 | assert.Equal(result.Success, true, "they should be equal") 18 | assert.Equal(result.Error, "", "they should be equal") 19 | }) 20 | 21 | t.Run("should return success response with custom message", func(t *testing.T) { 22 | result := successResponse("test") 23 | 24 | assert.IsType(JSONResponse{}, result) 25 | assert.Equal(result.Success, true, "they should be equal") 26 | assert.Equal(result.Error, "", "they should be equal") 27 | assert.Equal(result.Message, "test", "they should be equal") 28 | }) 29 | }) 30 | 31 | t.Run("errorResponse", func(t *testing.T) { 32 | t.Run("should return error response", func(t *testing.T) { 33 | result := errorResponse() 34 | 35 | assert.IsType(JSONResponse{}, result) 36 | assert.Equal(result.Success, false, "they should be equal") 37 | assert.Equal(result.Error, "An error occurred", "they should be equal") 38 | }) 39 | 40 | t.Run("should return error response with custom message", func(t *testing.T) { 41 | result := errorResponse("test") 42 | 43 | assert.IsType(JSONResponse{}, result) 44 | assert.Equal(result.Success, false, "they should be equal") 45 | assert.Equal(result.Error, "test", "they should be equal") 46 | }) 47 | }) 48 | } 49 | -------------------------------------------------------------------------------- /web/server.go: -------------------------------------------------------------------------------- 1 | // Package web includes code for the web server of PhoneInfoga 2 | // 3 | //go:generate swag init -g ./server.go --parseDependency 4 | package web 5 | 6 | import ( 7 | "github.com/gin-gonic/gin" 8 | v2 "github.com/sundowndev/phoneinfoga/v2/web/v2/api/server" 9 | "net/http" 10 | ) 11 | 12 | // @title PhoneInfoga REST API 13 | // @description Advanced information gathering & OSINT framework for phone numbers. 14 | // @version v2 15 | // @host localhost:5000 16 | // @BasePath /api 17 | // @schemes http https 18 | // @license.name GNU General Public License v3.0 19 | // @license.url https://github.com/sundowndev/phoneinfoga/blob/master/LICENSE 20 | 21 | type Server struct { 22 | router *gin.Engine 23 | } 24 | 25 | func NewServer(disableClient bool) (*Server, error) { 26 | s := &Server{ 27 | router: gin.Default(), 28 | } 29 | if err := s.registerRoutes(disableClient); err != nil { 30 | return s, err 31 | } 32 | return s, nil 33 | } 34 | 35 | func (s *Server) registerRoutes(disableClient bool) error { 36 | group := s.router.Group("/api") 37 | 38 | group. 39 | GET("/", healthHandler). 40 | GET("/numbers", getAllNumbers). 41 | GET("/numbers/:number/validate", ValidateScanURL, validate). 42 | GET("/numbers/:number/scan/local", ValidateScanURL, localScan). 43 | GET("/numbers/:number/scan/numverify", ValidateScanURL, numverifyScan). 44 | GET("/numbers/:number/scan/googlesearch", ValidateScanURL, googleSearchScan). 45 | GET("/numbers/:number/scan/ovh", ValidateScanURL, ovhScan) 46 | 47 | v2routes := v2.NewServer().Routes() 48 | for _, r := range v2routes { 49 | group.Handle(r.Method, r.Path, r.HandlerFunc) 50 | } 51 | 52 | if !disableClient { 53 | err := registerClientRoutes(s.router) 54 | if err != nil { 55 | return err 56 | } 57 | } 58 | 59 | s.router.Use(func(c *gin.Context) { 60 | c.JSON(404, JSONResponse{ 61 | Success: false, 62 | Error: "resource not found", 63 | }) 64 | }) 65 | 66 | return nil 67 | } 68 | 69 | func (s *Server) ListenAndServe(addr string) error { 70 | return s.router.Run(addr) 71 | } 72 | 73 | func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { 74 | s.router.ServeHTTP(w, r) 75 | } 76 | -------------------------------------------------------------------------------- /web/v2/api/handlers/init.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "github.com/sirupsen/logrus" 5 | "github.com/sundowndev/phoneinfoga/v2/lib/filter" 6 | "github.com/sundowndev/phoneinfoga/v2/lib/remote" 7 | "sync" 8 | ) 9 | 10 | var once sync.Once 11 | var RemoteLibrary *remote.Library 12 | 13 | func Init(filterEngine filter.Filter) { 14 | once.Do(func() { 15 | RemoteLibrary = remote.NewLibrary(filterEngine) 16 | remote.InitScanners(RemoteLibrary) 17 | logrus.Debug("Scanners and plugins initialized") 18 | }) 19 | } 20 | -------------------------------------------------------------------------------- /web/v2/api/handlers/init_test.go: -------------------------------------------------------------------------------- 1 | package handlers_test 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "github.com/sundowndev/phoneinfoga/v2/lib/filter" 6 | "github.com/sundowndev/phoneinfoga/v2/web/v2/api/handlers" 7 | "testing" 8 | ) 9 | 10 | func TestInit(t *testing.T) { 11 | handlers.Init(filter.NewEngine()) 12 | assert.NotNil(t, handlers.RemoteLibrary) 13 | assert.Greater(t, len(handlers.RemoteLibrary.GetAllScanners()), 0) 14 | } 15 | -------------------------------------------------------------------------------- /web/v2/api/handlers/numbers.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/sundowndev/phoneinfoga/v2/lib/number" 6 | "github.com/sundowndev/phoneinfoga/v2/web/v2/api" 7 | "net/http" 8 | ) 9 | 10 | type AddNumberInput struct { 11 | Number string `json:"number" binding:"number,required"` 12 | } 13 | 14 | type AddNumberResponse struct { 15 | Valid bool `json:"valid"` 16 | RawLocal string `json:"rawLocal"` 17 | Local string `json:"local"` 18 | E164 string `json:"e164"` 19 | International string `json:"international"` 20 | CountryCode int32 `json:"countryCode"` 21 | Country string `json:"country"` 22 | Carrier string `json:"carrier"` 23 | } 24 | 25 | // AddNumber is an HTTP handler 26 | // @ID AddNumber 27 | // @Tags Numbers 28 | // @Summary Add a new number. 29 | // @Description This route returns information about a given phone number. 30 | // @Accept json 31 | // @Produce json 32 | // @Param request body AddNumberInput true "Request body" 33 | // @Success 200 {object} AddNumberResponse 34 | // @Success 500 {object} api.ErrorResponse 35 | // @Router /v2/numbers [post] 36 | func AddNumber(ctx *gin.Context) *api.Response { 37 | var input AddNumberInput 38 | if err := ctx.ShouldBindJSON(&input); err != nil { 39 | return &api.Response{ 40 | Code: http.StatusBadRequest, 41 | JSON: true, 42 | Data: api.ErrorResponse{Error: "Invalid phone number: please provide an integer without any special chars"}, 43 | } 44 | } 45 | 46 | num, err := number.NewNumber(input.Number) 47 | if err != nil { 48 | return &api.Response{ 49 | Code: http.StatusBadRequest, 50 | JSON: true, 51 | Data: api.ErrorResponse{Error: err.Error()}, 52 | } 53 | } 54 | 55 | return &api.Response{ 56 | Code: http.StatusOK, 57 | JSON: true, 58 | Data: AddNumberResponse{ 59 | Valid: num.Valid, 60 | RawLocal: num.RawLocal, 61 | Local: num.Local, 62 | E164: num.E164, 63 | International: num.International, 64 | CountryCode: num.CountryCode, 65 | Country: num.Country, 66 | Carrier: num.Carrier, 67 | }, 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /web/v2/api/handlers/numbers_test.go: -------------------------------------------------------------------------------- 1 | package handlers_test 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "github.com/stretchr/testify/assert" 7 | "github.com/sundowndev/phoneinfoga/v2/web/v2/api" 8 | "github.com/sundowndev/phoneinfoga/v2/web/v2/api/handlers" 9 | "github.com/sundowndev/phoneinfoga/v2/web/v2/api/server" 10 | "net/http" 11 | "net/http/httptest" 12 | "testing" 13 | ) 14 | 15 | func TestAddNumber(t *testing.T) { 16 | type expectedResponse struct { 17 | Code int 18 | Body interface{} 19 | } 20 | 21 | testcases := []struct { 22 | Name string 23 | Input handlers.AddNumberInput 24 | Expected expectedResponse 25 | }{ 26 | { 27 | Name: "test successfully adding number", 28 | Input: handlers.AddNumberInput{Number: "14152229670"}, 29 | Expected: expectedResponse{ 30 | Code: 200, 31 | Body: handlers.AddNumberResponse{ 32 | Valid: true, 33 | RawLocal: "4152229670", 34 | Local: "(415) 222-9670", 35 | E164: "+14152229670", 36 | International: "14152229670", 37 | CountryCode: 1, 38 | Country: "US", 39 | Carrier: "", 40 | }, 41 | }, 42 | }, 43 | { 44 | Name: "test bad params", 45 | Input: handlers.AddNumberInput{Number: "a14152229670"}, 46 | Expected: expectedResponse{ 47 | Code: 400, 48 | Body: api.ErrorResponse{Error: "Invalid phone number: please provide an integer without any special chars"}, 49 | }, 50 | }, 51 | { 52 | Name: "test invalid number", 53 | Input: handlers.AddNumberInput{Number: "331"}, 54 | Expected: expectedResponse{ 55 | Code: 400, 56 | Body: api.ErrorResponse{Error: "the string supplied is too short to be a phone number"}, 57 | }, 58 | }, 59 | } 60 | 61 | for _, tt := range testcases { 62 | t.Run(tt.Name, func(t *testing.T) { 63 | r := server.NewServer() 64 | 65 | data, err := json.Marshal(&tt.Input) 66 | if err != nil { 67 | t.Fatal(err) 68 | } 69 | 70 | req, err := http.NewRequest(http.MethodPost, "/v2/numbers", bytes.NewReader(data)) 71 | if err != nil { 72 | t.Fatal(err) 73 | } 74 | w := httptest.NewRecorder() 75 | r.ServeHTTP(w, req) 76 | 77 | b, err := json.Marshal(tt.Expected.Body) 78 | if err != nil { 79 | t.Fatal(err) 80 | } 81 | 82 | assert.Equal(t, tt.Expected.Code, w.Code) 83 | assert.Equal(t, string(b), w.Body.String()) 84 | }) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /web/v2/api/handlers/scanners.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/sundowndev/phoneinfoga/v2/lib/number" 6 | "github.com/sundowndev/phoneinfoga/v2/lib/remote" 7 | "github.com/sundowndev/phoneinfoga/v2/web/v2/api" 8 | "net/http" 9 | ) 10 | 11 | type Scanner struct { 12 | Name string `json:"name"` 13 | Description string `json:"description"` 14 | } 15 | 16 | type GetAllScannersResponse struct { 17 | Scanners []Scanner `json:"scanners"` 18 | } 19 | 20 | // GetAllScanners is an HTTP handler 21 | // @ID GetAllScanners 22 | // @Tags Numbers 23 | // @Summary Get all available scanners. 24 | // @Description This route returns all available scanners. 25 | // @Produce json 26 | // @Success 200 {object} GetAllScannersResponse 27 | // @Router /v2/scanners [get] 28 | func GetAllScanners(*gin.Context) *api.Response { 29 | var scanners []Scanner 30 | for _, s := range RemoteLibrary.GetAllScanners() { 31 | scanners = append(scanners, Scanner{ 32 | Name: s.Name(), 33 | Description: s.Description(), 34 | }) 35 | } 36 | 37 | return &api.Response{ 38 | Code: http.StatusOK, 39 | JSON: true, 40 | Data: GetAllScannersResponse{ 41 | Scanners: scanners, 42 | }, 43 | } 44 | } 45 | 46 | type DryRunScannerInput struct { 47 | Number string `json:"number" binding:"number,required"` 48 | Options remote.ScannerOptions `json:"options" validate:"dive,required"` 49 | } 50 | 51 | type DryRunScannerResponse struct { 52 | Success bool `json:"success"` 53 | Error string `json:"error,omitempty"` 54 | } 55 | 56 | // DryRunScanner is an HTTP handler 57 | // @ID DryRunScanner 58 | // @Tags Numbers 59 | // @Summary Dry run a single scanner 60 | // @Description This route performs a dry run with the given phone number. This doesn't perform an actual scan. 61 | // @Accept json 62 | // @Produce json 63 | // @Param request body DryRunScannerInput true "Request body" 64 | // @Success 200 {object} DryRunScannerResponse 65 | // @Success 404 {object} api.ErrorResponse 66 | // @Success 500 {object} api.ErrorResponse 67 | // @Router /v2/scanners/{scanner}/dryrun [post] 68 | // @Param scanner path string true "Scanner name" validate(required) 69 | func DryRunScanner(ctx *gin.Context) *api.Response { 70 | var input DryRunScannerInput 71 | if err := ctx.ShouldBindJSON(&input); err != nil { 72 | return &api.Response{ 73 | Code: http.StatusBadRequest, 74 | JSON: true, 75 | Data: api.ErrorResponse{Error: "Invalid phone number: please provide an integer without any special chars"}, 76 | } 77 | } 78 | 79 | if input.Options == nil { 80 | input.Options = make(remote.ScannerOptions) 81 | } 82 | 83 | scanner := RemoteLibrary.GetScanner(ctx.Param("scanner")) 84 | if scanner == nil { 85 | return &api.Response{ 86 | Code: http.StatusNotFound, 87 | JSON: true, 88 | Data: api.ErrorResponse{Error: "Scanner not found"}, 89 | } 90 | } 91 | 92 | num, err := number.NewNumber(input.Number) 93 | if err != nil { 94 | return &api.Response{ 95 | Code: http.StatusBadRequest, 96 | JSON: true, 97 | Data: api.ErrorResponse{Error: err.Error()}, 98 | } 99 | } 100 | 101 | err = scanner.DryRun(*num, input.Options) 102 | if err != nil { 103 | return &api.Response{ 104 | Code: http.StatusBadRequest, 105 | JSON: true, 106 | Data: DryRunScannerResponse{ 107 | Success: false, 108 | Error: err.Error(), 109 | }, 110 | } 111 | } 112 | 113 | return &api.Response{ 114 | Code: http.StatusOK, 115 | JSON: true, 116 | Data: DryRunScannerResponse{ 117 | Success: true, 118 | }, 119 | } 120 | } 121 | 122 | type RunScannerInput struct { 123 | Number string `json:"number" binding:"number,required"` 124 | Options remote.ScannerOptions `json:"options" validate:"dive,required"` 125 | } 126 | 127 | type RunScannerResponse struct { 128 | Result interface{} `json:"result"` 129 | } 130 | 131 | // RunScanner is an HTTP handler 132 | // @ID RunScanner 133 | // @Tags Numbers 134 | // @Summary Run a single scanner 135 | // @Description This route runs a single scanner with the given phone number 136 | // @Accept json 137 | // @Produce json 138 | // @Param request body RunScannerInput true "Request body" 139 | // @Success 200 {object} RunScannerResponse 140 | // @Success 404 {object} api.ErrorResponse 141 | // @Success 500 {object} api.ErrorResponse 142 | // @Router /v2/scanners/{scanner}/run [post] 143 | // @Param scanner path string true "Scanner name" validate(required) 144 | func RunScanner(ctx *gin.Context) *api.Response { 145 | var input RunScannerInput 146 | if err := ctx.ShouldBindJSON(&input); err != nil { 147 | return &api.Response{ 148 | Code: http.StatusBadRequest, 149 | JSON: true, 150 | Data: api.ErrorResponse{Error: "Invalid phone number: please provide an integer without any special chars"}, 151 | } 152 | } 153 | 154 | if input.Options == nil { 155 | input.Options = make(remote.ScannerOptions) 156 | } 157 | 158 | scanner := RemoteLibrary.GetScanner(ctx.Param("scanner")) 159 | if scanner == nil { 160 | return &api.Response{ 161 | Code: http.StatusNotFound, 162 | JSON: true, 163 | Data: api.ErrorResponse{Error: "Scanner not found"}, 164 | } 165 | } 166 | 167 | num, err := number.NewNumber(input.Number) 168 | if err != nil { 169 | return &api.Response{ 170 | Code: http.StatusBadRequest, 171 | JSON: true, 172 | Data: api.ErrorResponse{Error: err.Error()}, 173 | } 174 | } 175 | 176 | result, err := scanner.Run(*num, input.Options) 177 | if err != nil { 178 | return &api.Response{ 179 | Code: http.StatusInternalServerError, 180 | JSON: true, 181 | Data: api.ErrorResponse{Error: err.Error()}, 182 | } 183 | } 184 | 185 | return &api.Response{ 186 | Code: http.StatusOK, 187 | JSON: true, 188 | Data: RunScannerResponse{ 189 | Result: result, 190 | }, 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /web/v2/api/response.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "net/http" 6 | ) 7 | 8 | type HandlerFunc func(ctx *gin.Context) *Response 9 | 10 | type Response struct { 11 | Code int 12 | Headers http.Header 13 | Data interface{} 14 | JSON bool 15 | } 16 | 17 | type ErrorResponse struct { 18 | Error string `json:"error"` 19 | } 20 | 21 | func WrapHandler(h HandlerFunc) gin.HandlerFunc { 22 | return func(ctx *gin.Context) { 23 | defer func() { 24 | if err := recover(); err != nil { 25 | ctx.AbortWithStatusJSON(500, ErrorResponse{Error: "Unknown error"}) 26 | } 27 | }() 28 | 29 | res := h(ctx) 30 | if res == nil { 31 | ctx.Abort() 32 | return 33 | } 34 | for key, values := range res.Headers { 35 | for _, val := range values { 36 | ctx.Writer.Header().Add(key, val) 37 | } 38 | } 39 | if res.JSON && res.Data != nil { 40 | ctx.JSON(res.Code, res.Data) 41 | return 42 | } 43 | ctx.Writer.WriteHeader(res.Code) 44 | if _, ok := res.Data.([]byte); ok { 45 | _, _ = ctx.Writer.Write(res.Data.([]byte)) 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /web/v2/api/response_test.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/stretchr/testify/assert" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | ) 10 | 11 | func TestWrapHandler(t *testing.T) { 12 | type expectedResult struct { 13 | Code int 14 | Headers http.Header 15 | Body string 16 | } 17 | 18 | testcases := []struct { 19 | Name string 20 | Handler HandlerFunc 21 | Expected expectedResult 22 | }{ 23 | { 24 | Name: "test basic handler", 25 | Handler: func(ctx *gin.Context) *Response { 26 | return &Response{ 27 | Code: 200, 28 | Headers: http.Header{ 29 | "My-Header": []string{"val1", "val2"}, 30 | }, 31 | Data: map[string]string{"msg": "test"}, 32 | JSON: true, 33 | } 34 | }, 35 | Expected: expectedResult{ 36 | Code: 200, 37 | Headers: http.Header{ 38 | "Content-Type": []string{"application/json; charset=utf-8"}, 39 | "My-Header": []string{"val1", "val2"}, 40 | }, 41 | Body: `{"msg":"test"}`, 42 | }, 43 | }, 44 | { 45 | Name: "test panic in handler", 46 | Handler: func(ctx *gin.Context) *Response { 47 | panic("dummy panic") 48 | }, 49 | Expected: expectedResult{ 50 | Code: 500, 51 | Headers: http.Header{"Content-Type": []string{"application/json; charset=utf-8"}}, 52 | Body: `{"error":"Unknown error"}`, 53 | }, 54 | }, 55 | { 56 | Name: "test byte response in handler", 57 | Handler: func(ctx *gin.Context) *Response { 58 | return &Response{ 59 | Code: 200, 60 | Headers: http.Header{}, 61 | Data: []byte("test"), 62 | } 63 | }, 64 | Expected: expectedResult{ 65 | Code: 200, 66 | Headers: http.Header{}, 67 | Body: `test`, 68 | }, 69 | }, 70 | { 71 | Name: "test unknown response in handler", 72 | Handler: func(ctx *gin.Context) *Response { 73 | return &Response{ 74 | Code: 200, 75 | Headers: http.Header{}, 76 | Data: 23, 77 | } 78 | }, 79 | Expected: expectedResult{ 80 | Code: 200, 81 | Headers: http.Header{}, 82 | Body: ``, 83 | }, 84 | }, 85 | { 86 | Name: "test nil response in handler", 87 | Handler: func(ctx *gin.Context) *Response { 88 | ctx.Writer.WriteHeader(403) 89 | _, _ = ctx.Writer.Write([]byte("test")) 90 | return nil 91 | }, 92 | Expected: expectedResult{ 93 | Code: 403, 94 | Headers: http.Header{}, 95 | Body: `test`, 96 | }, 97 | }, 98 | } 99 | 100 | for _, tt := range testcases { 101 | t.Run(tt.Name, func(t *testing.T) { 102 | e := gin.New() 103 | e.GET("/test", WrapHandler(tt.Handler)) 104 | 105 | req, err := http.NewRequest(http.MethodGet, "/test", nil) 106 | if err != nil { 107 | t.Fatal(err) 108 | } 109 | w := httptest.NewRecorder() 110 | e.ServeHTTP(w, req) 111 | 112 | assert.Equal(t, tt.Expected.Code, w.Code) 113 | assert.Equal(t, tt.Expected.Headers, w.Header()) 114 | assert.Equal(t, tt.Expected.Body, w.Body.String()) 115 | }) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /web/v2/api/server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "github.com/fatih/color" 5 | "github.com/gin-gonic/gin" 6 | "github.com/sundowndev/phoneinfoga/v2/web/v2/api" 7 | "github.com/sundowndev/phoneinfoga/v2/web/v2/api/handlers" 8 | "net/http" 9 | ) 10 | 11 | type Server struct { 12 | router *gin.Engine 13 | } 14 | 15 | func NewServer() *Server { 16 | gin.DefaultWriter = color.Output 17 | gin.DefaultErrorWriter = color.Error 18 | s := &Server{ 19 | router: gin.Default(), 20 | } 21 | s.registerRoutes() 22 | return s 23 | } 24 | 25 | func (s *Server) registerRoutes() { 26 | s.router.Group("/v2"). 27 | POST("/numbers", api.WrapHandler(handlers.AddNumber)). 28 | POST("/scanners/:scanner/dryrun", api.WrapHandler(handlers.DryRunScanner)). 29 | POST("/scanners/:scanner/run", api.WrapHandler(handlers.RunScanner)). 30 | GET("/scanners", api.WrapHandler(handlers.GetAllScanners)) 31 | } 32 | 33 | func (s *Server) Routes() gin.RoutesInfo { 34 | return s.router.Routes() 35 | } 36 | 37 | func (s *Server) ListenAndServe(addr string) error { 38 | return s.router.Run(addr) 39 | } 40 | 41 | func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { 42 | s.router.ServeHTTP(w, r) 43 | } 44 | -------------------------------------------------------------------------------- /web/validators.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | errors2 "errors" 5 | "github.com/gin-gonic/gin" 6 | "github.com/sundowndev/phoneinfoga/v2/web/errors" 7 | ) 8 | 9 | // JSONResponse is the default API response type 10 | type JSONResponse struct { 11 | Success bool `json:"success"` 12 | Error string `json:"error,omitempty"` 13 | Message string `json:"message,omitempty"` 14 | } 15 | 16 | type scanURL struct { 17 | Number uint `uri:"number" binding:"required,min=2"` 18 | } 19 | 20 | // ValidateScanURL validates scan URLs 21 | func ValidateScanURL(c *gin.Context) { 22 | var v scanURL 23 | if err := c.ShouldBindUri(&v); err != nil { 24 | handleError(c, errors.NewBadRequest(errors2.New("the given phone number is not valid"))) 25 | } 26 | } 27 | --------------------------------------------------------------------------------