The response has been limited to 50k tokens of the smallest files in the repo. You can remove this limitation by removing the max tokens filter.
├── .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 | [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fsundowndev%2FPhoneInfoga.svg?type=shield)](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 | [![DigitalOcean Referral Badge](https://web-platforms.sfo2.cdn.digitaloceanspaces.com/WWW/Badge%203.svg)](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 | ![](./images/screenshot.png)
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 | ![](/images/0e2SMdL.png)
23 | 
24 | Here’s the same phone number in E.164 formatting: +14155552671
25 | 
26 | ![](/images/KfrvacR.png)
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 |     "^.+\\.vue
quot;: "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 | 


--------------------------------------------------------------------------------