├── .codeclimate.yml ├── .dockerignore ├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md ├── stale.yml └── workflows │ ├── build.yml │ ├── client.yml │ ├── deploy-docs.yml │ ├── dockerimage.yml │ └── goreleaser.yml ├── .gitignore ├── .goreleaser.yml ├── CODEOWNERS ├── Dockerfile ├── LICENSE ├── README.md ├── api ├── controllers.go ├── openapi.yml ├── response.go ├── response_test.go ├── server.go ├── server_test.go └── validators.go ├── client ├── .gitignore ├── 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 │ ├── 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 │ │ └── 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 ├── cmd ├── recon.go ├── root.go ├── scan.go ├── serve.go └── version.go ├── docker-compose.traefik.yml ├── docker-compose.yml ├── docs ├── contribute.md ├── formatting.md ├── go-module-usage.md ├── images │ ├── banner.png │ ├── logo.svg │ ├── logo_white.svg │ └── screenshot.png ├── index.md ├── install.md ├── jetbrains.svg ├── resources.md └── usage.md ├── go.mod ├── go.sum ├── main.go ├── mkdocs.yml ├── pkg ├── config │ ├── config_test.go │ └── version.go ├── scanners │ ├── google.go │ ├── google_test.go │ ├── local.go │ ├── local_test.go │ ├── numverify.go │ ├── numverify_test.go │ ├── ovh.go │ ├── ovh_test.go │ └── scanners.go └── utils │ ├── logger.go │ ├── mocks │ └── Color.go │ ├── utils.go │ └── utils_test.go ├── renovate.json └── scripts ├── docker_push.sh └── format.sh /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | plugins: 2 | golint: 3 | enabled: true 4 | gofmt: 5 | enabled: true -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .idea 3 | .buildconfig 4 | __pycache__/ 5 | .vscode/ 6 | geckodriver.log -------------------------------------------------------------------------------- /.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/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: [push] 4 | 5 | jobs: 6 | build: 7 | name: Build 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Set up Go 1.14 11 | uses: actions/setup-go@v1 12 | with: 13 | go-version: 1.14 14 | id: go 15 | - name: Check out code into the Go module directory 16 | uses: actions/checkout@v1 17 | 18 | - name: Get dependencies 19 | run: | 20 | go get -v -t -d ./... 21 | if [ -f Gopkg.toml ]; then 22 | curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh 23 | dep ensure 24 | fi 25 | 26 | - name: Use Node.js ${{ matrix.node-version }} 27 | uses: actions/setup-node@v1 28 | with: 29 | node-version: 12.16.x 30 | 31 | - name: Building static assets 32 | run: (cd client && yarn && yarn build) 33 | 34 | - name: Build 35 | run: | 36 | go get -v github.com/jessevdk/go-assets-builder 37 | go generate ./... 38 | go build -v . 39 | 40 | - name: Lint 41 | run: | 42 | curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s v1.24.0 43 | ./bin/golangci-lint run -D errcheck 44 | 45 | - name: Test 46 | env: 47 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 48 | run: | 49 | go test -race -coverprofile=coverage.txt -covermode=atomic -v -bench=. ./... 50 | bash <(curl -s https://codecov.io/bash) 51 | -------------------------------------------------------------------------------- /.github/workflows/client.yml: -------------------------------------------------------------------------------- 1 | name: Web client CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | node-version: [12.18] 11 | steps: 12 | - uses: actions/checkout@v1 13 | - name: Use Node.js ${{ matrix.node-version }} 14 | uses: actions/setup-node@v1 15 | with: 16 | node-version: ${{ matrix.node-version }} 17 | - name: Install & lint 18 | run: | 19 | cd client 20 | yarn 21 | yarn lint 22 | - name: Build 23 | run: | 24 | cd client 25 | yarn build 26 | - name: Test 27 | run: | 28 | cd client 29 | yarn test:unit 30 | # - name: Upload coverage to Codecov 31 | # uses: codecov/codecov-action@v1.0.2 32 | # with: 33 | # token: ${{secrets.CODECOV_TOKEN}} 34 | # file: ./client/coverage/coverage-final.json 35 | # flags: unittests 36 | # name: codecov-umbrella 37 | -------------------------------------------------------------------------------- /.github/workflows/deploy-docs.yml: -------------------------------------------------------------------------------- 1 | name: Documentation CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v1 13 | - name: Set up Python 3.8 14 | uses: actions/setup-python@v1 15 | with: 16 | python-version: 3.8 17 | 18 | - name: Install dependencies 19 | run: | 20 | python -m pip install --upgrade pip 21 | pip install mkdocs==1.1 mkdocs-material==5.1.0 mkdocs-minify-plugin==0.3.0 22 | 23 | - name: Deploy 24 | run: | 25 | SSH_DIR="${HOME}/.ssh" 26 | mkdir "${SSH_DIR}" 27 | ssh-keyscan -t rsa github.com > "${SSH_DIR}/known_hosts" 28 | echo "${{ secrets.ACTIONS_DEPLOY_KEY }}" > "${SSH_DIR}/id_rsa" 29 | chmod 400 "${SSH_DIR}/id_rsa" 30 | git remote set-url origin git@github.com:sundowndev/PhoneInfoga.git 31 | git config user.name "${{ secrets.GITHUB_USER }}" 32 | git config user.email "${{ secrets.GITHUB_USER }}@users.noreply.github.com" 33 | mkdocs gh-deploy --force 34 | -------------------------------------------------------------------------------- /.github/workflows/dockerimage.yml: -------------------------------------------------------------------------------- 1 | name: Docker Image CI 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@master 12 | - name: Publish to Registry 13 | uses: elgohr/Publish-Docker-Github-Action@master 14 | with: 15 | name: sundowndev/phoneinfoga 16 | username: ${{ secrets.DOCKER_USERNAME }} 17 | password: ${{ secrets.DOCKER_PASSWORD }} 18 | 19 | #- uses: actions/checkout@master 20 | #- name: Deploy production 21 | # run: | 22 | # SSH_DIR="${HOME}/.ssh" 23 | # mkdir "${SSH_DIR}" 24 | # ssh-keyscan -t rsa xx.xx.xx.xx > "${SSH_DIR}/known_hosts" 25 | # echo "${{ secrets.PRODUCTION_DEPLOY_KEY }}" > "${SSH_DIR}/id_rsa" 26 | # chmod 400 "${SSH_DIR}/id_rsa" 27 | # ssh ${{ secrets.PRODUCTION_SSH_USER }}@xx.xx.xx.xx -p xxxx /bin/bash << EOF 28 | # IMAGE="sundowndev/phoneinfoga:latest" 29 | # docker pull $IMAGE; 30 | # docker restart $(docker ps | grep "$IMAGE" | awk '{ print $1 }') 31 | # EOF 32 | -------------------------------------------------------------------------------- /.github/workflows/goreleaser.yml: -------------------------------------------------------------------------------- 1 | name: goreleaser 2 | 3 | on: [push] 4 | 5 | jobs: 6 | goreleaser: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout 10 | uses: actions/checkout@v2 11 | 12 | - name: Unshallow 13 | run: git fetch --prune --unshallow 14 | 15 | - name: Use Node.js ${{ matrix.node-version }} 16 | uses: actions/setup-node@v1 17 | with: 18 | node-version: 12.16.x 19 | 20 | - name: Building static assets 21 | run: (cd client && yarn && yarn build) 22 | 23 | - name: Set up Go 24 | uses: actions/setup-go@v1 25 | with: 26 | go-version: 1.14.x 27 | 28 | - name: Install go-assets-builder 29 | run: | 30 | go get -v github.com/jessevdk/go-assets-builder 31 | 32 | - name: Run GoReleaser 33 | uses: goreleaser/goreleaser-action@v1 34 | with: 35 | version: latest 36 | args: release --rm-dist --skip-validate 37 | env: 38 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | .vscode/ 17 | 18 | .DS_Store 19 | coverage 20 | coverage.* 21 | .idea 22 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | before: 2 | hooks: 3 | - go generate ./... 4 | - go mod download 5 | builds: 6 | - env: 7 | - CGO_ENABLED=0 8 | - GO111MODULE=on 9 | binary: phoneinfoga 10 | goos: 11 | - linux 12 | - darwin 13 | goarch: 14 | - 386 15 | - amd64 16 | - arm 17 | - arm64 18 | - aarch64 19 | archives: 20 | - name_template: '{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}' 21 | replacements: 22 | darwin: Darwin 23 | linux: Linux 24 | windows: Windows 25 | 386: i386 26 | amd64: x86_64 27 | files: 28 | - none* 29 | checksum: 30 | name_template: '{{ .ProjectName }}_checksums.txt' 31 | snapshot: 32 | name_template: "{{ .Tag }}-next" 33 | changelog: 34 | sort: asc 35 | filters: 36 | exclude: 37 | - '^docs:' 38 | - '^test:' 39 | - '^chore:' 40 | - '^ci:' 41 | - Merge pull request 42 | - Merge branch 43 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @sundowndev 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12.18-alpine AS client_builder 2 | 3 | WORKDIR /app 4 | 5 | COPY ./client . 6 | 7 | RUN yarn 8 | 9 | RUN yarn build 10 | 11 | FROM golang:1.14-alpine as go_builder 12 | 13 | LABEL maintainer="Sundowndev" \ 14 | org.label-schema.name="phoneinfoga" \ 15 | org.label-schema.description="Advanced information gathering & OSINT tool for phone numbers." \ 16 | org.label-schema.url="https://github.com/sundowndev/PhoneInfoga" \ 17 | org.label-schema.vcs-url="https://github.com/sundowndev/PhoneInfoga" \ 18 | org.label-schema.vendor="Sundowndev" \ 19 | org.label-schema.schema-version="1.0" 20 | 21 | WORKDIR /app 22 | 23 | COPY . . 24 | 25 | RUN go get -v -t -d ./... 26 | 27 | COPY --from=client_builder /app/dist ./client/dist 28 | 29 | RUN go get -v github.com/jessevdk/go-assets-builder 30 | 31 | RUN go generate ./... 32 | 33 | RUN go build -v -o phoneinfoga . 34 | 35 | FROM golang:1.14-alpine 36 | 37 | WORKDIR /app 38 | 39 | COPY --from=go_builder /app/phoneinfoga . 40 | 41 | EXPOSE 5000 42 | 43 | ENTRYPOINT ["/app/phoneinfoga"] 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 |
6 | 7 | build status 8 | 9 | 10 | go report 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | Latest version 20 | 21 |
22 | 23 |

Advanced information gathering & OSINT framework for phone numbers

24 | 25 |

26 | Documentation • 27 | API documentation • 28 | Demo instance • 29 | Related blog post 30 |

31 | 32 | ![](./docs/images/screenshot.png) 33 | 34 | ## About 35 | 36 | PhoneInfoga is one of the most advanced tools to scan international phone numbers using only free resources. The goal is to first gather standard information such as country, area, carrier and line type on any international phone numbers with a very good accuracy. Then search for footprints on search engines to try to find the VoIP provider or identify the owner. 37 | 38 | ## Current status 39 | 40 | **This project is under active development but is stable and production-ready. Numverify scan is pointless on demo instance because my server's IP got blocked due to spam**. [Roadmap is here](https://github.com/sundowndev/PhoneInfoga/projects/1). 41 | 42 | This project has recently been rewritten in Go language (previously Python). Why ? To improve code base, maintainability, have a stronger test suite and be able to compile code base. PhoneInfoga v2 brings new features such as serving a REST API and a web client. Usage of scanners was improved in order to drop usage of Selenium/Geckodriver which has cause many users to have troubleshoots using the tool. You can still use the legacy version in [tag v1.11](https://github.com/sundowndev/PhoneInfoga/tree/v1.11) and the legacy Docker image (`sundowndev/phoneinfoga:legacy`). Some features were not included in the v2 MVP such as input/output CLI options. The roadmap of the project changed so we can focus on the web client features such as downloading scan results as CSV, Instagram/Whatsapp lookup, and more. **Version 2 does not scan Google results anymore**, [read more](https://sundowndev.github.io/PhoneInfoga/usage/#available-scanners). 43 | 44 | ## Features 45 | 46 | - Check if phone number exists and is possible 47 | - Gather standard informations such as country, line type and carrier 48 | - OSINT footprinting using external APIs, Google Hacking, phone books & search engines 49 | - Check for reputation reports, social media, disposable numbers and more 50 | - Scan several numbers at once 51 | - Use custom formatting for more effective OSINT reconnaissance 52 | - **NEW**: Serve a web client along with a REST API to run scans from the browser 53 | - **NEW**: Run your own web instance as a service 54 | - **NEW**: Programmatic usage with Go modules 55 | 56 | ## Anti-features 57 | 58 | - Does not claim to provide relevant or verified data, it's just a tool ! 59 | - Does not allow to "track" a phone or its owner in real time 60 | - Does not allow to get the precise phone location 61 | - Does not allow to hack a phone 62 | 63 | ![Footprinting process](https://i.imgur.com/qCkgzz8.png) 64 | 65 | ## License 66 | 67 | [![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) 68 | 69 | This tool is licensed under the GNU General Public License v3.0. 70 | 71 | [Icon](https://www.flaticon.com/free-icon/fingerprint-search-symbol-of-secret-service-investigation_48838) made by Freepik from flaticon.com is licensed by CC 3.0 BY. 72 | 73 | ## Support 74 | 75 | [![](docs/jetbrains.svg)](https://www.jetbrains.com/?from=sundowndev) 76 | 77 | Thanks to [JetBrains](https://www.jetbrains.com/?from=sundowndev) for supporting my open-source projects. 78 | -------------------------------------------------------------------------------- /api/controllers.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "gopkg.in/sundowndev/phoneinfoga.v2/pkg/config" 6 | "gopkg.in/sundowndev/phoneinfoga.v2/pkg/scanners" 7 | ) 8 | 9 | type scanResultResponse struct { 10 | JSONResponse 11 | Result interface{} `json:"result"` 12 | } 13 | 14 | type listNumbersResponse struct { 15 | JSONResponse 16 | Numbers []scanners.Number `json:"numbers"` 17 | } 18 | 19 | func getAllNumbers(c *gin.Context) { 20 | c.JSON(200, listNumbersResponse{ 21 | JSONResponse: JSONResponse{Success: true}, 22 | Numbers: []scanners.Number{}, 23 | }) 24 | } 25 | 26 | func validate(c *gin.Context) { 27 | c.JSON(200, successResponse("The number is valid")) 28 | } 29 | 30 | func localScan(c *gin.Context) { 31 | result, _ := c.Get("number") 32 | 33 | c.JSON(200, scanResultResponse{ 34 | JSONResponse: JSONResponse{Success: true}, 35 | Result: result.(*scanners.Number), 36 | }) 37 | } 38 | 39 | func numverifyScan(c *gin.Context) { 40 | number, _ := c.Get("number") 41 | 42 | result, err := scanners.NumverifyScan(number.(*scanners.Number)) 43 | 44 | if err != nil { 45 | c.JSON(500, errorResponse()) 46 | return 47 | } 48 | 49 | c.JSON(200, scanResultResponse{ 50 | JSONResponse: JSONResponse{Success: true}, 51 | Result: result, 52 | }) 53 | } 54 | 55 | func googleSearchScan(c *gin.Context) { 56 | number, _ := c.Get("number") 57 | 58 | result := scanners.GoogleSearchScan(number.(*scanners.Number)) 59 | 60 | c.JSON(200, scanResultResponse{ 61 | JSONResponse: JSONResponse{Success: true}, 62 | Result: result, 63 | }) 64 | } 65 | 66 | func ovhScan(c *gin.Context) { 67 | number, _ := c.Get("number") 68 | 69 | result, err := scanners.OVHScan(number.(*scanners.Number)) 70 | 71 | if err != nil { 72 | c.JSON(500, errorResponse()) 73 | return 74 | } 75 | 76 | c.JSON(200, scanResultResponse{ 77 | JSONResponse: JSONResponse{Success: true}, 78 | Result: result, 79 | }) 80 | } 81 | 82 | func healthHandler(c *gin.Context) { 83 | c.JSON(200, gin.H{ 84 | "success": true, 85 | "version": config.Version, 86 | }) 87 | } 88 | -------------------------------------------------------------------------------- /api/openapi.yml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | info: 3 | title: PhoneInfoga 4 | version: '1.0' 5 | description: 'PhoneInfoga is one of the most advanced tools to scan phone numbers using only free resources. The goal is to first gather standard information such as country, area, carrier and line type on any international phone numbers with a very good accuracy. Then search for footprints on search engines to try to find the VoIP provider or identify the owner.' 6 | servers: 7 | - url: 'http://localhost:3000/api' 8 | description: API URL 9 | paths: 10 | /numbers: 11 | get: 12 | summary: Get saved numbers 13 | tags: [] 14 | responses: 15 | '200': 16 | description: '' 17 | headers: {} 18 | content: 19 | application/json: 20 | schema: 21 | type: object 22 | properties: 23 | numbers: 24 | type: array 25 | items: 26 | $ref: '#/components/schemas/Number' 27 | examples: 28 | Success: 29 | value: 30 | numbers: 31 | - local: '2623' 32 | E164: '+2622623' 33 | international: '2622623' 34 | countryCode: 262 35 | country: RE 36 | carrier: '' 37 | Error: 38 | value: 39 | success: false 40 | message: An error occurred 41 | application/xml: 42 | schema: 43 | type: object 44 | properties: {} 45 | multipart/form-data: 46 | schema: 47 | type: object 48 | properties: {} 49 | '500': 50 | description: Internal Server Error 51 | content: 52 | application/json: 53 | schema: 54 | $ref: '#/components/schemas/ErrorResponse' 55 | examples: {} 56 | operationId: get-numbers 57 | description: |- 58 | Get all known numbers. 59 | 60 | This route is actually not used yet. 61 | parameters: [] 62 | requestBody: {} 63 | '/numbers/{number}/scan/local': 64 | parameters: 65 | - schema: 66 | type: string 67 | name: number 68 | in: path 69 | required: true 70 | get: 71 | summary: Local scan 72 | tags: [] 73 | responses: 74 | '200': 75 | description: OK 76 | headers: {} 77 | content: 78 | application/json: 79 | schema: 80 | type: object 81 | properties: 82 | success: 83 | type: boolean 84 | error: 85 | type: string 86 | result: 87 | $ref: '#/components/schemas/LocalScanResult' 88 | examples: 89 | Success: 90 | value: 91 | success: true 92 | error: '' 93 | result: 94 | local: '2623' 95 | E164: '+2622623' 96 | international: '2622623' 97 | countryCode: 262 98 | country: RE 99 | carrier: '' 100 | operationId: get-numbers-scan-number-local 101 | description: '' 102 | '/numbers/{number}/scan/numverify': 103 | parameters: 104 | - schema: 105 | type: string 106 | name: number 107 | in: path 108 | required: true 109 | get: 110 | summary: Numverify scan 111 | tags: [] 112 | responses: 113 | '200': 114 | description: OK 115 | content: 116 | application/json: 117 | schema: 118 | type: object 119 | properties: 120 | success: 121 | type: boolean 122 | error: 123 | type: string 124 | result: 125 | $ref: '#/components/schemas/NumverifyScanResult' 126 | operationId: get-numbers-number-scan-numverify 127 | '/numbers/{number}/scan/googlesearch': 128 | parameters: 129 | - schema: 130 | type: string 131 | name: number 132 | in: path 133 | required: true 134 | get: 135 | summary: Google search scan 136 | tags: [] 137 | responses: 138 | '200': 139 | description: OK 140 | content: 141 | application/json: 142 | schema: 143 | type: object 144 | properties: 145 | success: 146 | type: boolean 147 | error: 148 | type: string 149 | result: 150 | $ref: '#/components/schemas/NumverifyScanResult' 151 | operationId: get-numbers-number-scan-googlesearch 152 | '/numbers/{number}/scan/ovh': 153 | get: 154 | summary: OVH Telecom scan 155 | tags: [] 156 | responses: 157 | '200': 158 | description: OK 159 | content: 160 | application/json: 161 | schema: 162 | type: object 163 | properties: 164 | success: 165 | type: boolean 166 | error: 167 | type: string 168 | result: 169 | $ref: '#/components/schemas/OVHScanResult' 170 | operationId: get-numbers-number-scan-ovh 171 | components: 172 | schemas: 173 | Number: 174 | title: Number 175 | type: object 176 | description: A phone number 177 | properties: 178 | id: 179 | type: integer 180 | number: 181 | type: integer 182 | countryISO: 183 | type: integer 184 | country: 185 | type: string 186 | carrier: 187 | type: string 188 | ErrorResponse: 189 | title: ErrorResponse 190 | type: object 191 | description: Generic error response schema. 192 | x-examples: 193 | Example: 194 | success: false 195 | message: An error occurred 196 | properties: 197 | success: 198 | type: boolean 199 | message: 200 | type: 201 | - string 202 | - 'null' 203 | required: 204 | - success 205 | - message 206 | LocalScanResult: 207 | title: LocalScanResult 208 | type: object 209 | properties: 210 | local: 211 | type: string 212 | E164: 213 | type: string 214 | international: 215 | type: string 216 | countryCode: 217 | type: number 218 | country: 219 | type: string 220 | carrier: 221 | type: string 222 | NumverifyScanResult: 223 | title: NumverifyScanResult 224 | type: object 225 | properties: 226 | valid: 227 | type: boolean 228 | number: 229 | type: string 230 | local_format: 231 | type: string 232 | international_format: 233 | type: string 234 | country_prefix: 235 | type: string 236 | country_code: 237 | type: string 238 | country_name: 239 | type: string 240 | location: 241 | type: string 242 | carrier: 243 | type: string 244 | line_type: 245 | type: string 246 | description: '' 247 | GoogleSearchScanResult: 248 | title: GoogleSearchScanResult 249 | type: object 250 | properties: 251 | socialMedia: 252 | type: array 253 | uniqueItems: false 254 | items: 255 | type: object 256 | properties: 257 | URL: 258 | type: string 259 | dork: 260 | type: string 261 | number: 262 | type: string 263 | disposableProviders: 264 | type: array 265 | items: 266 | type: object 267 | properties: 268 | URL: 269 | type: string 270 | dork: 271 | type: string 272 | number: 273 | type: string 274 | reputation: 275 | type: array 276 | items: 277 | type: object 278 | properties: 279 | URL: 280 | type: string 281 | dork: 282 | type: string 283 | number: 284 | type: string 285 | individuals: 286 | type: array 287 | items: 288 | type: object 289 | properties: 290 | URL: 291 | type: string 292 | dork: 293 | type: string 294 | number: 295 | type: string 296 | general: 297 | type: array 298 | items: 299 | type: object 300 | properties: 301 | URL: 302 | type: string 303 | dork: 304 | type: string 305 | number: 306 | type: string 307 | OVHScanResult: 308 | title: OVHScanResult 309 | type: object 310 | properties: 311 | found: 312 | type: boolean 313 | numberRange: 314 | type: string 315 | city: 316 | type: string 317 | zipCode: 318 | type: string 319 | securitySchemes: {} 320 | -------------------------------------------------------------------------------- /api/response.go: -------------------------------------------------------------------------------- 1 | package api 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 | Error: 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 | -------------------------------------------------------------------------------- /api/response_test.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestResponse(t *testing.T) { 10 | assert := assert.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, "test", "they should be equal") 27 | }) 28 | }) 29 | 30 | t.Run("errorResponse", func(t *testing.T) { 31 | t.Run("should return error response", func(t *testing.T) { 32 | result := errorResponse() 33 | 34 | assert.IsType(JSONResponse{}, result) 35 | assert.Equal(result.Success, false, "they should be equal") 36 | assert.Equal(result.Error, "An error occurred", "they should be equal") 37 | }) 38 | 39 | t.Run("should return error response with custom message", func(t *testing.T) { 40 | result := errorResponse("test") 41 | 42 | assert.IsType(JSONResponse{}, result) 43 | assert.Equal(result.Success, false, "they should be equal") 44 | assert.Equal(result.Error, "test", "they should be equal") 45 | }) 46 | }) 47 | } 48 | -------------------------------------------------------------------------------- /api/server.go: -------------------------------------------------------------------------------- 1 | // Package api is about the REST API of PhoneInfoga 2 | //go:generate $GOPATH/bin/go-assets-builder ../client/dist -o ./assets.go -p api 3 | package api 4 | 5 | import ( 6 | "net/http" 7 | "strings" 8 | 9 | "github.com/gin-gonic/gin" 10 | ) 11 | 12 | const ( 13 | clientDistPath = "/client/dist/" 14 | staticPath = "/" 15 | ) 16 | 17 | func detectContentType(path string, data []byte) string { 18 | arr := strings.Split(path, ".") 19 | ext := arr[len(arr)-1] 20 | 21 | switch ext { 22 | case "js": 23 | return "application/javascript" 24 | case "css": 25 | return "text/css" 26 | case "svg": 27 | return "image/svg+xml" 28 | default: 29 | return http.DetectContentType(data) 30 | } 31 | } 32 | 33 | func registerClientRoute(router *gin.Engine) { 34 | for name, file := range Assets.Files { 35 | if file.IsDir() { 36 | continue 37 | } 38 | 39 | path := strings.ReplaceAll(name, clientDistPath, staticPath) 40 | data := file.Data 41 | 42 | if path == staticPath+"index.html" { 43 | path = staticPath 44 | } 45 | 46 | router.GET(path, func(c *gin.Context) { 47 | c.Header("Content-Type", detectContentType(path, data)) 48 | c.Writer.WriteHeader(http.StatusOK) 49 | c.Writer.Write(data) 50 | c.Abort() 51 | }) 52 | } 53 | } 54 | 55 | // Serve launches the web client 56 | // Using Gin & Vue.js 57 | func Serve(router *gin.Engine, disableClient bool) *gin.Engine { 58 | router.Group("/api"). 59 | GET("/", healthHandler). 60 | GET("/numbers", getAllNumbers). 61 | GET("/numbers/:number/validate", ValidateScanURL, validate). 62 | GET("/numbers/:number/scan/local", ValidateScanURL, localScan). 63 | GET("/numbers/:number/scan/numverify", ValidateScanURL, numverifyScan). 64 | GET("/numbers/:number/scan/googlesearch", ValidateScanURL, googleSearchScan). 65 | GET("/numbers/:number/scan/ovh", ValidateScanURL, ovhScan) 66 | 67 | if !disableClient { 68 | registerClientRoute(router) 69 | } 70 | 71 | router.Use(func(c *gin.Context) { 72 | c.JSON(404, JSONResponse{ 73 | Success: false, 74 | Error: "Resource not found", 75 | }) 76 | }) 77 | 78 | return router 79 | } 80 | -------------------------------------------------------------------------------- /api/server_test.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "io/ioutil" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | 9 | "github.com/gin-gonic/gin" 10 | assertTest "github.com/stretchr/testify/assert" 11 | "gopkg.in/h2non/gock.v1" 12 | "gopkg.in/sundowndev/phoneinfoga.v2/pkg/config" 13 | "gopkg.in/sundowndev/phoneinfoga.v2/pkg/scanners" 14 | ) 15 | 16 | var r *gin.Engine 17 | 18 | func performRequest(r http.Handler, method, path string) (*httptest.ResponseRecorder, error) { 19 | req, err := http.NewRequest(method, path, nil) 20 | w := httptest.NewRecorder() 21 | r.ServeHTTP(w, req) 22 | return w, err 23 | } 24 | 25 | func BenchmarkAPI(b *testing.B) { 26 | assert := assertTest.New(b) 27 | r = gin.Default() 28 | r = Serve(r, true) 29 | 30 | b.Run("localScan - /api/numbers/:number/scan/local", func(b *testing.B) { 31 | for i := 0; i < b.N; i++ { 32 | res, err := performRequest(r, "GET", "/api/numbers/3312345253/scan/local") 33 | assert.Equal(nil, err) 34 | assert.Equal(res.Result().StatusCode, 200, "should be equal") 35 | } 36 | }) 37 | } 38 | 39 | func TestApi(t *testing.T) { 40 | assert := assertTest.New(t) 41 | r = gin.Default() 42 | r = Serve(r, false) 43 | 44 | t.Run("detectContentType", func(t *testing.T) { 45 | contentType := detectContentType("/file.hash.css", []byte{}) 46 | assert.Equal("text/css", contentType, "should be equal") 47 | 48 | contentType = detectContentType("/file.hash.js", []byte{}) 49 | assert.Equal("application/javascript", contentType, "should be equal") 50 | 51 | contentType = detectContentType("/file.hash.svg", []byte{}) 52 | assert.Equal("image/svg+xml", contentType, "should be equal") 53 | 54 | contentType = detectContentType("/file.html", []byte("")) 55 | assert.Equal("text/html; charset=utf-8", contentType, "should be equal") 56 | }) 57 | 58 | t.Run("Serve", func(t *testing.T) { 59 | t.Run("getAllNumbers - /api/numbers", func(t *testing.T) { 60 | res, err := performRequest(r, "GET", "/api/numbers") 61 | 62 | body, _ := ioutil.ReadAll(res.Body) 63 | 64 | assert.Equal(err, nil, "should be equal") 65 | assert.Equal(res.Result().StatusCode, 200, "should be equal") 66 | assert.Equal(string(body), "{\"success\":true,\"error\":\"\",\"numbers\":[]}", "should be equal") 67 | }) 68 | 69 | t.Run("validate - /api/numbers/:number/validate", func(t *testing.T) { 70 | t.Run("valid number", func(t *testing.T) { 71 | res, err := performRequest(r, "GET", "/api/numbers/3312345253/validate") 72 | 73 | body, _ := ioutil.ReadAll(res.Body) 74 | 75 | assert.Equal(err, nil, "should be equal") 76 | assert.Equal(res.Result().StatusCode, 200, "should be equal") 77 | assert.Equal(string(body), "{\"success\":true,\"error\":\"The number is valid\"}", "should be equal") 78 | }) 79 | 80 | t.Run("invalid number", func(t *testing.T) { 81 | res, err := performRequest(r, "GET", "/api/numbers/azerty/validate") 82 | 83 | body, _ := ioutil.ReadAll(res.Body) 84 | 85 | assert.Equal(err, nil, "should be equal") 86 | assert.Equal(res.Result().StatusCode, 400, "should be equal") 87 | assert.Equal(string(body), "{\"success\":false,\"error\":\"Parameter 'number' must be a valid integer.\"}", "should be equal") 88 | }) 89 | 90 | t.Run("invalid country code", func(t *testing.T) { 91 | res, err := performRequest(r, "GET", "/api/numbers/09880/validate") 92 | 93 | body, _ := ioutil.ReadAll(res.Body) 94 | 95 | assert.Equal(err, nil, "should be equal") 96 | assert.Equal(res.Result().StatusCode, 400, "should be equal") 97 | assert.Equal(string(body), "{\"success\":false,\"error\":\"invalid country code\"}", "should be equal") 98 | }) 99 | }) 100 | 101 | t.Run("localScan - /api/numbers/:number/scan/local", func(t *testing.T) { 102 | t.Run("valid number", func(t *testing.T) { 103 | res, err := performRequest(r, "GET", "/api/numbers/3312345253/scan/local") 104 | 105 | body, _ := ioutil.ReadAll(res.Body) 106 | 107 | assert.Equal(err, nil, "should be equal") 108 | assert.Equal(res.Result().StatusCode, 200, "should be equal") 109 | assert.Equal(string(body), `{"success":true,"error":"","result":{"rawLocal":"12345253","local":"12345253","E164":"+3312345253","international":"3312345253","countryCode":33,"country":"FR","carrier":""}}`, "should be equal") 110 | }) 111 | 112 | t.Run("invalid number", func(t *testing.T) { 113 | res, err := performRequest(r, "GET", "/api/numbers/9999999999/scan/local") 114 | 115 | body, _ := ioutil.ReadAll(res.Body) 116 | 117 | assert.Equal(err, nil, "should be equal") 118 | assert.Equal(res.Result().StatusCode, 400, "should be equal") 119 | assert.Equal(string(body), `{"success":false,"error":"invalid country code"}`, "should be equal") 120 | }) 121 | }) 122 | 123 | t.Run("numverifyScan - /api/numbers/:number/scan/numverify", func(t *testing.T) { 124 | t.Run("should succeed", func(t *testing.T) { 125 | defer gock.Off() // Flush pending mocks after test execution 126 | 127 | expectedResult := scanners.NumverifyScannerResponse{ 128 | Valid: true, 129 | Number: "79516566591", 130 | LocalFormat: "9516566591", 131 | InternationalFormat: "+79516566591", 132 | CountryPrefix: "+7", 133 | CountryCode: "RU", 134 | CountryName: "Russian Federation", 135 | Location: "Saint Petersburg and Leningrad Oblast", 136 | Carrier: "OJSC St. Petersburg Telecom (OJSC Tele2-Saint-Petersburg)", 137 | LineType: "mobile", 138 | } 139 | 140 | gock.New("http://numverify.com"). 141 | Get("/"). 142 | Reply(200).BodyString(``) 143 | 144 | gock.New("https://numverify.com"). 145 | Get("/php_helper_scripts/phone_api.php"). 146 | MatchParam("secret_key", "5ad5554ac240e4d3d31107941b35a5eb"). 147 | MatchParam("number", "79516566591"). 148 | Reply(200). 149 | JSON(expectedResult) 150 | 151 | res, err := performRequest(r, "GET", "/api/numbers/79516566591/scan/numverify") 152 | 153 | body, _ := ioutil.ReadAll(res.Body) 154 | 155 | assert.Equal(err, nil, "should be equal") 156 | assert.Equal(res.Result().StatusCode, 200, "should be equal") 157 | assert.Equal(string(body), `{"success":true,"error":"","result":{"valid":true,"number":"79516566591","local_format":"9516566591","international_format":"+79516566591","country_prefix":"+7","country_code":"RU","country_name":"Russian Federation","location":"Saint Petersburg and Leningrad Oblast","carrier":"OJSC St. Petersburg Telecom (OJSC Tele2-Saint-Petersburg)","line_type":"mobile"}}`, "should be equal") 158 | 159 | assert.Equal(gock.IsDone(), true, "there should have no pending mocks") 160 | }) 161 | }) 162 | 163 | t.Run("googleSearchScan - /api/numbers/:number/scan/googlesearch", func(t *testing.T) { 164 | t.Run("should return google search dorks", func(t *testing.T) { 165 | res, err := performRequest(r, "GET", "/api/numbers/330365179268/scan/googlesearch") 166 | 167 | body, _ := ioutil.ReadAll(res.Body) 168 | 169 | assert.Equal(err, nil, "should be equal") 170 | assert.Equal(res.Result().StatusCode, 200, "should be equal") 171 | assert.Equal(string(body), `{"success":true,"error":"","result":{"socialMedia":[{"number":"+33365179268","dork":"site:facebook.com intext:\"33365179268\" OR intext:\"+33365179268\" OR intext:\"0365179268\"","URL":"https://www.google.com/search?q=site%3Afacebook.com+intext%3A%2233365179268%22+OR+intext%3A%22%2B33365179268%22+OR+intext%3A%220365179268%22"},{"number":"+33365179268","dork":"site:twitter.com intext:\"33365179268\" OR intext:\"+33365179268\" OR intext:\"0365179268\"","URL":"https://www.google.com/search?q=site%3Atwitter.com+intext%3A%2233365179268%22+OR+intext%3A%22%2B33365179268%22+OR+intext%3A%220365179268%22"},{"number":"+33365179268","dork":"site:linkedin.com intext:\"33365179268\" OR intext:\"+33365179268\" OR intext:\"0365179268\"","URL":"https://www.google.com/search?q=site%3Alinkedin.com+intext%3A%2233365179268%22+OR+intext%3A%22%2B33365179268%22+OR+intext%3A%220365179268%22"},{"number":"+33365179268","dork":"site:instagram.com intext:\"33365179268\" OR intext:\"+33365179268\" OR intext:\"0365179268\"","URL":"https://www.google.com/search?q=site%3Ainstagram.com+intext%3A%2233365179268%22+OR+intext%3A%22%2B33365179268%22+OR+intext%3A%220365179268%22"},{"number":"+33365179268","dork":"site:vk.com intext:\"33365179268\" OR intext:\"+33365179268\" OR intext:\"0365179268\"","URL":"https://www.google.com/search?q=site%3Avk.com+intext%3A%2233365179268%22+OR+intext%3A%22%2B33365179268%22+OR+intext%3A%220365179268%22"}],"disposableProviders":[{"number":"+33365179268","dork":"site:hs3x.com intext:\"33365179268\"","URL":"https://www.google.com/search?q=site%3Ahs3x.com+intext%3A%2233365179268%22"},{"number":"+33365179268","dork":"site:receive-sms-now.com intext:\"33365179268\" OR intext:\"0365179268\"","URL":"https://www.google.com/search?q=site%3Areceive-sms-now.com+intext%3A%2233365179268%22+OR+intext%3A%220365179268%22"},{"number":"+33365179268","dork":"site:smslisten.com intext:\"33365179268\" OR intext:\"0365179268\"","URL":"https://www.google.com/search?q=site%3Asmslisten.com+intext%3A%2233365179268%22+OR+intext%3A%220365179268%22"},{"number":"+33365179268","dork":"site:smsnumbersonline.com intext:\"33365179268\" OR intext:\"0365179268\"","URL":"https://www.google.com/search?q=site%3Asmsnumbersonline.com+intext%3A%2233365179268%22+OR+intext%3A%220365179268%22"},{"number":"+33365179268","dork":"site:freesmscode.com intext:\"33365179268\" OR intext:\"0365179268\"","URL":"https://www.google.com/search?q=site%3Afreesmscode.com+intext%3A%2233365179268%22+OR+intext%3A%220365179268%22"},{"number":"+33365179268","dork":"site:catchsms.com intext:\"33365179268\" OR intext:\"0365179268\"","URL":"https://www.google.com/search?q=site%3Acatchsms.com+intext%3A%2233365179268%22+OR+intext%3A%220365179268%22"},{"number":"+33365179268","dork":"site:smstibo.com intext:\"33365179268\" OR intext:\"0365179268\"","URL":"https://www.google.com/search?q=site%3Asmstibo.com+intext%3A%2233365179268%22+OR+intext%3A%220365179268%22"},{"number":"+33365179268","dork":"site:smsreceiving.com intext:\"33365179268\" OR intext:\"0365179268\"","URL":"https://www.google.com/search?q=site%3Asmsreceiving.com+intext%3A%2233365179268%22+OR+intext%3A%220365179268%22"},{"number":"+33365179268","dork":"site:getfreesmsnumber.com intext:\"33365179268\" OR intext:\"0365179268\"","URL":"https://www.google.com/search?q=site%3Agetfreesmsnumber.com+intext%3A%2233365179268%22+OR+intext%3A%220365179268%22"},{"number":"+33365179268","dork":"site:sellaite.com intext:\"33365179268\" OR intext:\"0365179268\"","URL":"https://www.google.com/search?q=site%3Asellaite.com+intext%3A%2233365179268%22+OR+intext%3A%220365179268%22"},{"number":"+33365179268","dork":"site:receive-sms-online.info intext:\"33365179268\" OR intext:\"0365179268\"","URL":"https://www.google.com/search?q=site%3Areceive-sms-online.info+intext%3A%2233365179268%22+OR+intext%3A%220365179268%22"},{"number":"+33365179268","dork":"site:receivesmsonline.com intext:\"33365179268\" OR intext:\"0365179268\"","URL":"https://www.google.com/search?q=site%3Areceivesmsonline.com+intext%3A%2233365179268%22+OR+intext%3A%220365179268%22"},{"number":"+33365179268","dork":"site:receive-a-sms.com intext:\"33365179268\" OR intext:\"0365179268\"","URL":"https://www.google.com/search?q=site%3Areceive-a-sms.com+intext%3A%2233365179268%22+OR+intext%3A%220365179268%22"},{"number":"+33365179268","dork":"site:sms-receive.net intext:\"33365179268\" OR intext:\"0365179268\"","URL":"https://www.google.com/search?q=site%3Asms-receive.net+intext%3A%2233365179268%22+OR+intext%3A%220365179268%22"},{"number":"+33365179268","dork":"site:receivefreesms.com intext:\"33365179268\" OR intext:\"0365179268\"","URL":"https://www.google.com/search?q=site%3Areceivefreesms.com+intext%3A%2233365179268%22+OR+intext%3A%220365179268%22"},{"number":"+33365179268","dork":"site:receive-sms.com intext:\"33365179268\" OR intext:\"0365179268\"","URL":"https://www.google.com/search?q=site%3Areceive-sms.com+intext%3A%2233365179268%22+OR+intext%3A%220365179268%22"},{"number":"+33365179268","dork":"site:receivetxt.com intext:\"33365179268\" OR intext:\"0365179268\"","URL":"https://www.google.com/search?q=site%3Areceivetxt.com+intext%3A%2233365179268%22+OR+intext%3A%220365179268%22"},{"number":"+33365179268","dork":"site:freephonenum.com intext:\"33365179268\" OR intext:\"0365179268\"","URL":"https://www.google.com/search?q=site%3Afreephonenum.com+intext%3A%2233365179268%22+OR+intext%3A%220365179268%22"},{"number":"+33365179268","dork":"site:freesmsverification.com intext:\"33365179268\" OR intext:\"0365179268\"","URL":"https://www.google.com/search?q=site%3Afreesmsverification.com+intext%3A%2233365179268%22+OR+intext%3A%220365179268%22"},{"number":"+33365179268","dork":"site:receive-sms-online.com intext:\"33365179268\" OR intext:\"0365179268\"","URL":"https://www.google.com/search?q=site%3Areceive-sms-online.com+intext%3A%2233365179268%22+OR+intext%3A%220365179268%22"},{"number":"+33365179268","dork":"site:smslive.co intext:\"33365179268\" OR intext:\"0365179268\"","URL":"https://www.google.com/search?q=site%3Asmslive.co+intext%3A%2233365179268%22+OR+intext%3A%220365179268%22"}],"reputation":[{"number":"+33365179268","dork":"site:whosenumber.info intext:\"+33365179268\" intitle:\"who called\"","URL":"https://www.google.com/search?q=site%3Awhosenumber.info+intext%3A%22%2B33365179268%22+intitle%3A%22who+called%22"},{"number":"+33365179268","dork":"intitle:\"Phone Fraud\" intext:\"33365179268\" OR intext:\"+33365179268\" OR intext:\"0365179268\"","URL":"https://www.google.com/search?q=intitle%3A%22Phone+Fraud%22+intext%3A%2233365179268%22+OR+intext%3A%22%2B33365179268%22+OR+intext%3A%220365179268%22"},{"number":"+33365179268","dork":"site:findwhocallsme.com intext:\"+33365179268\" OR intext:\"33365179268\"","URL":"https://www.google.com/search?q=site%3Afindwhocallsme.com+intext%3A%22%2B33365179268%22+OR+intext%3A%2233365179268%22"},{"number":"+33365179268","dork":"site:yellowpages.ca intext:\"+33365179268\"","URL":"https://www.google.com/search?q=site%3Ayellowpages.ca+intext%3A%22%2B33365179268%22"},{"number":"+33365179268","dork":"site:phonenumbers.ie intext:\"+33365179268\"","URL":"https://www.google.com/search?q=site%3Aphonenumbers.ie+intext%3A%22%2B33365179268%22"},{"number":"+33365179268","dork":"site:who-calledme.com intext:\"+33365179268\"","URL":"https://www.google.com/search?q=site%3Awho-calledme.com+intext%3A%22%2B33365179268%22"},{"number":"+33365179268","dork":"site:usphonesearch.net intext:\"0365179268\"","URL":"https://www.google.com/search?q=site%3Ausphonesearch.net+intext%3A%220365179268%22"},{"number":"+33365179268","dork":"site:whocalled.us inurl:\"0365179268\"","URL":"https://www.google.com/search?q=site%3Awhocalled.us+inurl%3A%220365179268%22"},{"number":"+33365179268","dork":"site:quinumero.info intext:\"0365179268\" OR intext:\"33365179268\"","URL":"https://www.google.com/search?q=site%3Aquinumero.info+intext%3A%220365179268%22+OR+intext%3A%2233365179268%22"},{"number":"+33365179268","dork":"site:uk.popularphotolook.com inurl:\"0365179268\"","URL":"https://www.google.com/search?q=site%3Auk.popularphotolook.com+inurl%3A%220365179268%22"}],"individuals":[{"number":"+33365179268","dork":"site:numinfo.net intext:\"33365179268\" OR intext:\"+33365179268\" OR intext:\"0365179268\"","URL":"https://www.google.com/search?q=site%3Anuminfo.net+intext%3A%2233365179268%22+OR+intext%3A%22%2B33365179268%22+OR+intext%3A%220365179268%22"},{"number":"+33365179268","dork":"site:sync.me intext:\"33365179268\" OR intext:\"+33365179268\" OR intext:\"0365179268\"","URL":"https://www.google.com/search?q=site%3Async.me+intext%3A%2233365179268%22+OR+intext%3A%22%2B33365179268%22+OR+intext%3A%220365179268%22"},{"number":"+33365179268","dork":"site:whocallsyou.de intext:\"0365179268\"","URL":"https://www.google.com/search?q=site%3Awhocallsyou.de+intext%3A%220365179268%22"},{"number":"+33365179268","dork":"site:pastebin.com intext:\"33365179268\" OR intext:\"+33365179268\" OR intext:\"0365179268\"","URL":"https://www.google.com/search?q=site%3Apastebin.com+intext%3A%2233365179268%22+OR+intext%3A%22%2B33365179268%22+OR+intext%3A%220365179268%22"},{"number":"+33365179268","dork":"site:whycall.me intext:\"33365179268\" OR intext:\"+33365179268\" OR intext:\"0365179268\"","URL":"https://www.google.com/search?q=site%3Awhycall.me+intext%3A%2233365179268%22+OR+intext%3A%22%2B33365179268%22+OR+intext%3A%220365179268%22"},{"number":"+33365179268","dork":"site:locatefamily.com intext:\"33365179268\" OR intext:\"+33365179268\" OR intext:\"0365179268\"","URL":"https://www.google.com/search?q=site%3Alocatefamily.com+intext%3A%2233365179268%22+OR+intext%3A%22%2B33365179268%22+OR+intext%3A%220365179268%22"},{"number":"+33365179268","dork":"site:spytox.com intext:\"0365179268\"","URL":"https://www.google.com/search?q=site%3Aspytox.com+intext%3A%220365179268%22"}],"general":[{"number":"+33365179268","dork":"intext:\"33365179268\" OR intext:\"+33365179268\" OR intext:\"0365179268\" OR intext:\"03 65 17 92 68\"","URL":"https://www.google.com/search?q=intext%3A%2233365179268%22+OR+intext%3A%22%2B33365179268%22+OR+intext%3A%220365179268%22+OR+intext%3A%2203+65+17+92+68%22"},{"number":"+33365179268","dork":"(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:xls) intext:\"33365179268\" OR intext:\"+33365179268\" OR intext:\"0365179268\"","URL":"https://www.google.com/search?q=%28ext%3Adoc+OR+ext%3Adocx+OR+ext%3Aodt+OR+ext%3Apdf+OR+ext%3Artf+OR+ext%3Asxw+OR+ext%3Apsw+OR+ext%3Appt+OR+ext%3Apptx+OR+ext%3Apps+OR+ext%3Acsv+OR+ext%3Atxt+OR+ext%3Axls%29+intext%3A%2233365179268%22+OR+intext%3A%22%2B33365179268%22+OR+intext%3A%220365179268%22"}]}}`, "should be equal") 172 | }) 173 | }) 174 | 175 | t.Run("ovhScan - /api/numbers/:number/scan/ovh", func(t *testing.T) { 176 | t.Run("should find number on OVH", func(t *testing.T) { 177 | defer gock.Off() // Flush pending mocks after test execution 178 | 179 | gock.New("https://api.ovh.com"). 180 | Get("/1.0/telephony/number/detailedZones"). 181 | MatchParam("country", "fr"). 182 | Reply(200). 183 | JSON([]scanners.OVHAPIResponseNumber{ 184 | { 185 | ZneList: []string{}, 186 | MatchingCriteria: "", 187 | Prefix: 33, 188 | InternationalNumber: "003336517xxxx", 189 | Country: "fr", 190 | ZipCode: "", 191 | Number: "036517xxxx", 192 | City: "Abbeville", 193 | AskedCity: "", 194 | }, 195 | }) 196 | 197 | res, err := performRequest(r, "GET", "/api/numbers/330365179268/scan/ovh") 198 | 199 | body, _ := ioutil.ReadAll(res.Body) 200 | 201 | assert.Equal(err, nil, "should be equal") 202 | assert.Equal(res.Result().StatusCode, 200, "should be equal") 203 | assert.Equal(string(body), `{"success":true,"error":"","result":{"found":true,"numberRange":"036517xxxx","city":"Abbeville","zipCode":""}}`, "should be equal") 204 | 205 | assert.Equal(gock.IsDone(), true, "there should have no pending mocks") 206 | }) 207 | }) 208 | 209 | t.Run("healthHandler - /api/", func(t *testing.T) { 210 | res, err := performRequest(r, "GET", "/api/") 211 | 212 | body, _ := ioutil.ReadAll(res.Body) 213 | 214 | assert.Equal(nil, err, "should be equal") 215 | assert.Equal(200, res.Result().StatusCode, "should be equal") 216 | assert.Equal("{\"success\":true,\"version\":\""+config.Version+"\"}", string(body), "should be equal") 217 | }) 218 | 219 | t.Run("404 error - /api/notfound", func(t *testing.T) { 220 | res, err := performRequest(r, "GET", "/api/notfound") 221 | 222 | body, _ := ioutil.ReadAll(res.Body) 223 | 224 | assert.Equal(err, nil, "should be equal") 225 | assert.Equal(res.Result().StatusCode, 404, "should be equal") 226 | assert.Equal(string(body), "{\"success\":false,\"error\":\"Resource not found\"}", "should be equal") 227 | }) 228 | 229 | t.Run("Client - /", func(t *testing.T) { 230 | res, err := performRequest(r, "GET", "/") 231 | 232 | assert.Equal(nil, err, "should be equal") 233 | assert.Equal(200, res.Result().StatusCode, "should be equal") 234 | assert.Equal(http.Header{"Content-Type": []string{"text/html; charset=utf-8"}}, res.Header(), "should be equal") 235 | }) 236 | }) 237 | } 238 | -------------------------------------------------------------------------------- /api/validators.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | "gopkg.in/sundowndev/phoneinfoga.v2/pkg/scanners" 8 | ) 9 | 10 | // JSONResponse is the default API response type 11 | type JSONResponse struct { 12 | Success bool `json:"success"` 13 | Error string `json:"error"` 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 | 24 | if err := c.ShouldBindUri(&v); err != nil { 25 | errorHandling(c, "Parameter 'number' must be a valid integer.") 26 | return 27 | } 28 | 29 | number, err := scanners.LocalScan(c.Param("number")) 30 | 31 | if err != nil { 32 | errorHandling(c, err.Error()) 33 | return 34 | } 35 | 36 | c.Set("number", number) 37 | } 38 | 39 | func errorHandling(c *gin.Context, msg string) { 40 | c.JSON(http.StatusBadRequest, JSONResponse{Success: false, Error: msg}) 41 | c.Abort() 42 | } 43 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /client/cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "pluginsFile": "tests/e2e/plugins/index.js" 3 | } 4 | -------------------------------------------------------------------------------- /client/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | transform: { 3 | "^.+\\.vue$": "vue-jest", 4 | ".+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$": 5 | "jest-transform-stub", 6 | "^.+\\.tsx?$": "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 | -------------------------------------------------------------------------------- /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.19.2", 17 | "bootstrap": "4.5.0", 18 | "bootstrap-vue": "2.15.0", 19 | "jquery": "3.5.1", 20 | "popper.js": "1.16.1", 21 | "vue": "2.6.11", 22 | "vue-class-component": "7.2.3", 23 | "vue-property-decorator": "8.5.0", 24 | "vue-router": "3.3.4", 25 | "vuex": "3.4.0" 26 | }, 27 | "devDependencies": { 28 | "@types/jest": "26.0.0", 29 | "@typescript-eslint/eslint-plugin": "2.34.0", 30 | "@typescript-eslint/parser": "2.34.0", 31 | "@vue/cli-plugin-e2e-cypress": "4.4.4", 32 | "@vue/cli-plugin-eslint": "4.4.4", 33 | "@vue/cli-plugin-router": "4.4.4", 34 | "@vue/cli-plugin-typescript": "4.4.4", 35 | "@vue/cli-plugin-unit-jest": "4.4.4", 36 | "@vue/cli-plugin-vuex": "4.4.4", 37 | "@vue/cli-service": "4.4.4", 38 | "@vue/eslint-config-prettier": "6.0.0", 39 | "@vue/eslint-config-typescript": "5.0.2", 40 | "@vue/test-utils": "1.0.3", 41 | "babel-core": "7.0.0-bridge.0", 42 | "eslint": "7.2.0", 43 | "eslint-plugin-prettier": "3.1.4", 44 | "eslint-plugin-vue": "6.2.2", 45 | "jest": "26.0.1", 46 | "prettier": "2.0.5", 47 | "ts-jest": "26.1.0", 48 | "typescript": "3.9.5", 49 | "vue-jest": "3.0.5", 50 | "vue-template-compiler": "2.6.11" 51 | }, 52 | "eslintConfig": { 53 | "root": true, 54 | "env": { 55 | "node": true 56 | }, 57 | "extends": [ 58 | "plugin:vue/essential", 59 | "eslint:recommended", 60 | "@vue/typescript/recommended", 61 | "@vue/prettier", 62 | "@vue/prettier/@typescript-eslint" 63 | ], 64 | "parserOptions": { 65 | "ecmaVersion": 2020 66 | }, 67 | "rules": {}, 68 | "overrides": [ 69 | { 70 | "files": [ 71 | "**/__tests__/*.{j,t}s?(x)", 72 | "**/tests/unit/**/*.spec.{j,t}s?(x)" 73 | ], 74 | "env": { 75 | "jest": true 76 | } 77 | } 78 | ] 79 | }, 80 | "browserslist": [ 81 | "> 1%", 82 | "last 2 versions" 83 | ] 84 | } 85 | -------------------------------------------------------------------------------- /client/public/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | PhoneInfoga 9 | 10 | 11 | 12 | 13 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /client/src/App.vue: -------------------------------------------------------------------------------- 1 | 89 | 90 | 110 | -------------------------------------------------------------------------------- /client/src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /client/src/components/GoogleSearch.vue: -------------------------------------------------------------------------------- 1 | 140 | 141 | 228 | -------------------------------------------------------------------------------- /client/src/components/LocalScan.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 70 | -------------------------------------------------------------------------------- /client/src/components/NumverifyScan.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 92 | -------------------------------------------------------------------------------- /client/src/components/OVHScan.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 87 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 NotFound from "../views/NotFound.vue"; 5 | 6 | Vue.use(VueRouter); 7 | 8 | const routes = [ 9 | { 10 | path: "/", 11 | name: "Scan", 12 | component: Scan, 13 | }, 14 | { 15 | path: "*", 16 | name: "NotFound", 17 | component: NotFound, 18 | }, 19 | ]; 20 | 21 | const router = new VueRouter({ 22 | mode: "hash", 23 | base: process.env.BASE_URL, 24 | routes, 25 | }); 26 | 27 | export default router; 28 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /client/src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.vue" { 2 | import Vue from "vue"; 3 | export default Vue; 4 | } 5 | -------------------------------------------------------------------------------- /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 = 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 | -------------------------------------------------------------------------------- /client/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | const formatNumber = (number: string): string => { 2 | return number.replace(/[_\W]+/g, ""); 3 | }; 4 | 5 | const isValid = (number: string): boolean => { 6 | const formatted = formatNumber(number); 7 | 8 | return formatted.match(/^[0-9]+$/) !== null && formatted.length > 2; 9 | }; 10 | 11 | export { formatNumber, isValid }; 12 | -------------------------------------------------------------------------------- /client/src/views/NotFound.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /client/src/views/Scan.vue: -------------------------------------------------------------------------------- 1 | 48 | 49 | 118 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /client/vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | publicPath: "/", 3 | }; 4 | -------------------------------------------------------------------------------- /cmd/recon.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | "gopkg.in/sundowndev/phoneinfoga.v2/pkg/utils" 6 | ) 7 | 8 | func init() { 9 | // Register command 10 | rootCmd.AddCommand(reconCmd) 11 | 12 | // Register flags 13 | reconCmd.PersistentFlags().StringVarP(&number, "number", "n", "", "The phone number to scan (E164 or international format)") 14 | reconCmd.PersistentFlags().StringVarP(&input, "input", "i", "", "Text file containing a list of phone numbers to scan (one per line)") 15 | reconCmd.PersistentFlags().StringVarP(&output, "output", "o", "", "Output to save scan results") 16 | } 17 | 18 | var reconCmd = &cobra.Command{ 19 | Deprecated: "Not implemented yet.", 20 | Use: "recon", 21 | Short: "Launch custom format reconnaissance", 22 | Run: func(cmd *cobra.Command, args []string) { 23 | utils.LoggerService.Infoln("Custom recon for phone number", number) 24 | 25 | utils.LoggerService.Errorln("Not implemented yet.") 26 | 27 | utils.LoggerService.Infoln("Job finished.") 28 | }, 29 | } 30 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | var number string 11 | var input string // TODO: implement input 12 | var output string // TODO: implement output 13 | 14 | var rootCmd = &cobra.Command{ 15 | Use: "phoneinfoga [COMMANDS] [OPTIONS]", 16 | Short: "Advanced information gathering & OSINT tool for phone numbers", 17 | Long: "PhoneInfoga is one of the most advanced tools to scan phone numbers using only free resources.", 18 | Example: "phoneinfoga scan -n ", 19 | } 20 | 21 | // Execute is a function that executes the root command 22 | func Execute() { 23 | if err := rootCmd.Execute(); err != nil { 24 | fmt.Println(err) 25 | os.Exit(1) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /cmd/scan.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/spf13/cobra" 7 | "gopkg.in/sundowndev/phoneinfoga.v2/pkg/scanners" 8 | "gopkg.in/sundowndev/phoneinfoga.v2/pkg/utils" 9 | ) 10 | 11 | func init() { 12 | // Register command 13 | rootCmd.AddCommand(scanCmd) 14 | 15 | // Register flags 16 | scanCmd.PersistentFlags().StringVarP(&number, "number", "n", "", "The phone number to scan (E164 or international format)") 17 | // scanCmd.PersistentFlags().StringVarP(&input, "input", "i", "", "Text file containing a list of phone numbers to scan (one per line)") 18 | // scanCmd.PersistentFlags().StringVarP(&output, "output", "o", "", "Output to save scan results") 19 | } 20 | 21 | var scanCmd = &cobra.Command{ 22 | Use: "scan", 23 | Short: "Scan a phone number", 24 | Run: func(cmd *cobra.Command, args []string) { 25 | utils.LoggerService.Infoln("Scanning phone number", number) 26 | 27 | if valid := utils.IsValid(number); !valid { 28 | utils.LoggerService.Errorln("Number is not valid.") 29 | os.Exit(1) 30 | } 31 | 32 | scanners.ScanCLI(number) 33 | 34 | utils.LoggerService.Infoln("Job finished.") 35 | }, 36 | } 37 | -------------------------------------------------------------------------------- /cmd/serve.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "strconv" 7 | 8 | "github.com/gin-gonic/gin" 9 | "github.com/spf13/cobra" 10 | api "gopkg.in/sundowndev/phoneinfoga.v2/api" 11 | ) 12 | 13 | var httpPort int 14 | var disableClient bool 15 | 16 | func init() { 17 | // Register command 18 | rootCmd.AddCommand(serveCmd) 19 | 20 | // Register flags 21 | serveCmd.PersistentFlags().IntVarP(&httpPort, "port", "p", 5000, "HTTP port") 22 | serveCmd.PersistentFlags().BoolVar(&disableClient, "no-client", false, "Disable web client (REST API only)") 23 | } 24 | 25 | var serveCmd = &cobra.Command{ 26 | Use: "serve", 27 | Short: "Serve web client", 28 | Run: func(cmd *cobra.Command, args []string) { 29 | router := gin.Default() 30 | 31 | api.Serve(router, disableClient) 32 | 33 | httpPort := ":" + strconv.Itoa(httpPort) 34 | 35 | srv := &http.Server{ 36 | Addr: httpPort, 37 | Handler: router, 38 | } 39 | 40 | if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { 41 | log.Fatalf("listen: %s\n", err) 42 | } 43 | }, 44 | } 45 | -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | "gopkg.in/sundowndev/phoneinfoga.v2/pkg/config" 6 | "gopkg.in/sundowndev/phoneinfoga.v2/pkg/utils" 7 | ) 8 | 9 | func init() { 10 | // Register command 11 | rootCmd.AddCommand(versionCmd) 12 | } 13 | 14 | var versionCmd = &cobra.Command{ 15 | Use: "version", 16 | Short: "Print current version of the tool", 17 | Run: func(cmd *cobra.Command, args []string) { 18 | utils.LoggerService.Infoln("PhoneInfoga", config.Version) 19 | utils.LoggerService.Infoln("Coded by Sundowndev https://github.com/sundowndev/PhoneInfoga") 20 | }, 21 | } 22 | -------------------------------------------------------------------------------- /docker-compose.traefik.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | 3 | services: 4 | phoneinfoga: 5 | # Disabled by default because it prevent scaling service 6 | # container_name: phoneinfoga 7 | restart: on-failure 8 | image: sundowndev/phoneinfoga:latest 9 | command: 10 | - "serve" 11 | networks: 12 | - default 13 | - web 14 | environment: 15 | - GIN_MODE=release 16 | labels: 17 | - 'traefik.docker.network=web' 18 | - 'traefik.enable=true' 19 | - 'traefik.domain=demo.phoneinfoga.crvx.fr' 20 | - 'traefik.basic.frontend.rule=Host:demo.phoneinfoga.crvx.fr' 21 | - 'traefik.basic.port=5000' 22 | - 'traefik.basic.protocol=http' 23 | - 'traefik.frontend.headers.SSLRedirect=true' 24 | - 'traefik.frontend.headers.STSSeconds=315360000' 25 | - 'traefik.frontend.headers.browserXSSFilter=true' 26 | - 'traefik.frontend.headers.contentTypeNosniff=true' 27 | - 'traefik.frontend.headers.forceSTSHeader=true' 28 | - "traefik.frontend.headers.contentSecurityPolicy=default-src 'self';frame-ancestors 'self';style-src 'self';script-src 'self';img-src 'self';font-src 'self'" 29 | - 'traefik.frontend.headers.referrerPolicy=no-referrer' 30 | - 'traefik.frontend.headers.frameDeny=true' 31 | 32 | networks: 33 | web: 34 | external: true 35 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docs/contribute.md: -------------------------------------------------------------------------------- 1 | # Contribute 2 | 3 | This page describe the project structure and gives you a bit of help to start contributing. 4 | 5 | The project is maintained by a single person: [sundowndev](https://github.com/sundowndev). Contributions are welcome ! 6 | 7 | !!! tip "Want to contribute ? Clone the project and open some pull requests !" 8 | 9 | ## Project 10 | 11 | ### Building from source 12 | 13 | **Requirements :** 14 | 15 | - Node.js >= v10.x 16 | - npm or yarn 17 | - Go >= 1.13 18 | 19 | **Note:** if you're using npm, just replace `yarn ` by `npm run `. 20 | 21 | ```shell 22 | # Build static assets 23 | # This will create dist directory containing client's static files 24 | $ (cd client && yarn && yarn build) 25 | 26 | # Generate in-memory assets 27 | $ go get -v github.com/jessevdk/go-assets-builder 28 | # This will put content of dist directory in memory. It's usually needed to build but 29 | # the design requires you to do it anyway. 30 | # This step is needed at each change if you're developing on the client. 31 | $ go generate ./... 32 | 33 | # Build the whole project 34 | $ go build -v . 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 | api # REST API code 47 | client # web client code 48 | cmd # Command-line app code 49 | docs # Documentation 50 | pkg # Code base for scanners, utils ... 51 | scripts # Development & deployment scripts 52 | go.mod # Go modules file 53 | main.go # Application entrypoint 54 | ``` 55 | 56 | ## Testing 57 | 58 | ### Go code 59 | 60 | ```shell 61 | # Run test suite 62 | go test -v ./... 63 | 64 | # Collect coverage 65 | go test -coverprofile=coverage.out ./... 66 | 67 | # Open coverage file as HTML 68 | go tool cover -html=coverage.out 69 | ``` 70 | 71 | ### Typescript code 72 | 73 | Developping on the web client. 74 | 75 | ```shell 76 | cd client 77 | 78 | yarn test 79 | yarn test:unit 80 | yarn test:e2e 81 | ``` 82 | 83 | If you're developing on the client, you can watch changes with `yarn build:watch`. 84 | 85 | ## Formatting 86 | 87 | ### Go code 88 | 89 | We use a shell script to format Go files. 90 | 91 | ```shell 92 | sh ./scripts/format.sh 93 | 94 | # You can also use GolangCI 95 | golangci-lint run -D errcheck 96 | ``` 97 | 98 | ### Typescript code 99 | 100 | ```shell 101 | cd client 102 | 103 | yarn lint 104 | yarn lint:fix 105 | ``` 106 | 107 | ## Documentation 108 | 109 | We use [mkdocs](https://www.mkdocs.org/) to write our documentation. 110 | 111 | ### Install mkdocs 112 | 113 | ```shell 114 | python3 -m pip install mkdocs 115 | ``` 116 | 117 | ### Serve documentation on localhost 118 | 119 | This is the only command you need to start working on docs. 120 | 121 | ```shell 122 | mkdocs serve 123 | # or 124 | python3 -m mkdocs serve 125 | ``` 126 | 127 | ### Build website 128 | 129 | ```shell 130 | mkdocs build 131 | ``` 132 | 133 | ### Deploy on github pages 134 | 135 | ```shell 136 | mkdocs gh-deploy 137 | ``` 138 | -------------------------------------------------------------------------------- /docs/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 | ![](https://i.imgur.com/0e2SMdL.png) 23 | 24 | Here’s the same phone number in E.164 formatting: +14155552671 25 | 26 | ![](https://i.imgur.com/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". So the tool use a (optional) custom formatting given by the user to find further and more accurate results. To use this feature properly and make the results more valuable, try to use a format that someone of the number' 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 | 80 | ## Local scan formatting (developers) 81 | 82 | The local scan create several variables to be usable in OSINT footprinting and other scanners. 83 | 84 | Examples : 85 | 86 | ``` 87 | { 88 | 'input': '+1 512-618-xxxx', 89 | 'default': '1512618xxxx', 90 | 'local': '512618xxxx', 91 | 'international': '+1 512-618-xxxx', 92 | 'country': 'United States', 93 | 'countryCode': '+1', 94 | 'countryIsoCode': 'US', 95 | 'location': 'Texas', 96 | 'carrier': '' 97 | } 98 | ``` 99 | 100 | ``` 101 | { 102 | 'input': '+33 066651xxxx', 103 | 'default': '3366651xxxx', 104 | 'local': '66651xxxx', 105 | 'international': '+33 66651xxxx', 106 | 'country': 'France', 107 | 'countryCode': '+33', 108 | 'countryIsoCode': 'FR', 109 | 'location': 'France', 110 | 'carrier': 'Bouygues' 111 | } 112 | ``` -------------------------------------------------------------------------------- /docs/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://godoc.org/github.com/sundowndev/PhoneInfoga). 4 | 5 | ### Install the module 6 | 7 | ``` 8 | go get -v gopkg.in/sundowndev/phoneinfoga.v2 9 | ``` 10 | 11 | ### Usage example 12 | 13 | ```go 14 | package main 15 | 16 | import ( 17 | "fmt" 18 | "log" 19 | 20 | phoneinfoga "gopkg.in/sundowndev/phoneinfoga.v2/pkg/scanners" 21 | ) 22 | 23 | func main() { 24 | number, err := phoneinfoga.LocalScan("") 25 | 26 | if err != nil { 27 | log.Fatal(err) 28 | } 29 | 30 | links := phoneinfoga.GoogleSearchScan(number) 31 | 32 | for _, link := range links.Individuals { 33 | fmt.Println(link.URL) // Google search link to scan 34 | } 35 | } 36 | 37 | ``` 38 | -------------------------------------------------------------------------------- /docs/images/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drumplayer123456/git-clone-https-github.com-Wes974-PhoneInfoga.../1e4646bfcc81058b5b4318ebc728097da60ead02/docs/images/banner.png -------------------------------------------------------------------------------- /docs/images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /docs/images/logo_white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /docs/images/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drumplayer123456/git-clone-https-github.com-Wes974-PhoneInfoga.../1e4646bfcc81058b5b4318ebc728097da60ead02/docs/images/screenshot.png -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Welcome to the PhoneInfoga documentation 2 | 3 | PhoneInfoga is one of the most advanced tools to scan phone numbers using only free resources. The goal is to first gather standard information such as country, area, carrier and line type on any international phone numbers with a very good accuracy. Then search for footprints on search engines to try to find the VoIP provider or identify the owner. 4 | 5 | ### [Read the related blog post](https://medium.com/@SundownDEV/phone-number-scanning-osint-recon-tool-6ad8f0cac27b) 6 | 7 | ## Features 8 | 9 | - Check if phone number exists and is possible 10 | - Gather standard informations such as country, line type and carrier 11 | - OSINT footprinting using external APIs, Google Hacking, phone books & search engines 12 | - Check for reputation reports, social media, disposable numbers and more 13 | - Scan several numbers at once 14 | - Use custom formatting for more effective OSINT reconnaissance 15 | - **NEW**: Serve a web client along with a REST API to run scans from the browser 16 | - **NEW**: Run your own web instance as a service 17 | - **NEW**: Programmatic usage with Go modules 18 | 19 | ## Anti-features 20 | 21 | - Does not claim to provide relevant or verified data, it's just a tool ! 22 | - Does not allow to "track" a phone or its owner in real time 23 | - Does not allow to get the precise phone location 24 | - Does not allow to hack a phone 25 | ---- 26 | 27 | !!! bug 28 | Found a bug ? Feel free to [open an issue](https://github.com/sundowndev/PhoneInfoga/issues). 29 | 30 | You can also [reach me on Twitter](https://twitter.com/sundowndev) or at raphael(at)crvx.fr. 31 | 32 | PGP: [B64687AB97F268F43E67B97A8916203E540C65A4](https://crvx.fr/publickey.asc) 33 | -------------------------------------------------------------------------------- /docs/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 and MacOS 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 always build from source by yourself. Want your OS to be supported ? Please [open an issue on GitHub](https://github.com/sundowndev/PhoneInfoga/issues). 5 | 6 | ## Binary installation (recommanded) 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: 15 | 16 | ```shell 17 | # Download the archive 18 | curl -L "https://github.com/sundowndev/phoneinfoga/releases/download/v2.0.8/phoneinfoga_$(uname -s)_$(uname -m).tar.gz" -o phoneinfoga.tar.gz 19 | 20 | # Extract the binary 21 | tar xfv phoneinfoga.tar.gz 22 | 23 | # Run the software 24 | ./phoneinfoga --help 25 | 26 | # You can install it globally 27 | mv ./phoneinfoga /usr/bin/phoneinfoga 28 | ``` 29 | 30 | If the installation fails, it probably means your OS/arch is not suppored. 31 | 32 | 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). 33 | 34 | ## Using Docker 35 | 36 | ### From docker hub 37 | 38 | You can pull the repository directly from Docker hub 39 | 40 | ```shell 41 | docker pull sundowndev/phoneinfoga:latest 42 | ``` 43 | 44 | Then run the tool 45 | 46 | ```shell 47 | docker run --rm -it sundowndev/phoneinfoga version 48 | ``` 49 | 50 | ### Docker-compose 51 | 52 | You can use a single docker-compose file to run the tool without downloading the source code. 53 | 54 | ``` 55 | version: '3.7' 56 | 57 | services: 58 | phoneinfoga: 59 | container_name: phoneinfoga 60 | restart: on-failure 61 | image: phoneinfoga:latest 62 | command: serve 63 | ports: 64 | - "80:5000" 65 | ``` 66 | 67 | ### From the source code 68 | 69 | You can download the source code, then build the docker images 70 | 71 | #### Build 72 | 73 | Build the image 74 | 75 | ```shell 76 | docker-compose build 77 | ``` 78 | 79 | #### CLI usage 80 | 81 | ```shell 82 | docker-compose run --rm phoneinfoga --help 83 | ``` 84 | 85 | #### Run web services 86 | 87 | ```shell 88 | docker-compose up -d 89 | ``` 90 | 91 | ##### Disable web client 92 | 93 | Edit `docker-compose.yml` and add the `--no-client` option 94 | 95 | ```yaml 96 | # docker-compose.yml 97 | command: "serve --no-client" 98 | ``` 99 | 100 | #### Troubleshooting 101 | 102 | All output is sent to stdout so it can be inspected by running: 103 | 104 | ```shell 105 | docker logs -f 106 | ``` 107 | -------------------------------------------------------------------------------- /docs/jetbrains.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 14 | 15 | 16 | 17 | 18 | 19 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 31 | 32 | 33 | 34 | 35 | 36 | 39 | 40 | 41 | 42 | 43 | 45 | 47 | 48 | 51 | 54 | 56 | 57 | 59 | 63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /docs/resources.md: -------------------------------------------------------------------------------- 1 | # Resources 2 | 3 | ### Inderstanding 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 | ``` -------------------------------------------------------------------------------- /docs/usage.md: -------------------------------------------------------------------------------- 1 | Here is the documentation for CLI usage. 2 | 3 | ```shell 4 | $ phoneinfoga 5 | 6 | PhoneInfoga is one of the most advanced tools to scan phone numbers using only free resources. 7 | 8 | Usage: 9 | phoneinfoga [command] 10 | 11 | Examples: 12 | phoneinfoga scan -n 13 | 14 | Available Commands: 15 | help Help about any command 16 | scan Scan a phone number 17 | serve Serve web client 18 | version Print current version of the tool 19 | 20 | Flags: 21 | -h, --help help for phoneinfoga 22 | 23 | Use "phoneinfoga [command] --help" for more information about a command. 24 | ``` 25 | 26 | ### Running a scan 27 | 28 | Use the `scan` command with the `-n` (or `--number`) option. 29 | 30 | ``` 31 | phoneinfoga scan -n "+1 (555) 444-1212" 32 | phoneinfoga scan -n "+33 06 79368229" 33 | phoneinfoga scan -n "33679368229" 34 | ``` 35 | 36 | Special chars such as `( ) - +` will be escaped so typing US-based numbers stay easy : 37 | 38 | ``` 39 | phoneinfoga scan -n "+1 555-444-3333" 40 | ``` 41 | 42 | !!! note "Note that the country code is essential. You don't know which country code to use ? [Find it here](https://www.countrycode.org/)" 43 | 44 | 69 | 70 | ## Available scanners 71 | 72 | - Numverify 73 | - Google search 74 | - OVH 75 | 76 | **Numverify** provide standard but useful informations such as number's country code, location, line type and carrier. 77 | 78 | **OVH** is, besides being a web and cloud hosting company, a telecom provider with several VoIP numbers in Europe. Thanks to their API-key free [REST API](https://api.ovh.com/), we are able to tell if a number is owned by OVH Telecom or not. 79 | 80 | **Google search** uses Google search engine and [Google Dorks](https://en.wikipedia.org/wiki/Google_hacking) to search phone number's footprints everywhere on the web. It allows you to search for scam reports, social media profiles, documents and more. **This scanner does only one thing:** generating several Google search links from a given phone number. You then have to manually open them in your browser to see results. You may therefore have links that do not return any results. 81 | 82 | ## Launch web client & REST API 83 | 84 | Run the tool through a REST API with a web client. The API has been written in Go and web client in Vue.js. 85 | 86 | ```shell 87 | phoneinfoga serve 88 | phoneinfoga serve -p 8080 # default port is 5000 89 | ``` 90 | 91 | You should then be able to see the web client at `http://localhost:`. 92 | 93 | ![](./images/screenshot.png) 94 | 95 | ### Run the REST API only 96 | 97 | You can choose to only run the REST API without the web client : 98 | 99 | ``` 100 | phoneinfoga serve --no-client 101 | ``` 102 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module gopkg.in/sundowndev/phoneinfoga.v2 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/PuerkitoBio/goquery v1.5.1 7 | github.com/fatih/color v1.9.0 8 | github.com/gin-gonic/gin v1.6.3 9 | github.com/golang/protobuf v1.3.4 // indirect 10 | github.com/hashicorp/golang-lru v0.5.4 // indirect 11 | github.com/jessevdk/go-assets v0.0.0-20160921144138-4f4301a06e15 12 | github.com/mattn/go-colorable v0.1.6 // indirect 13 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 14 | github.com/modern-go/reflect2 v1.0.1 // indirect 15 | github.com/nyaruka/phonenumbers v1.0.56 16 | github.com/onlinecity/go-phone-iso3166 v0.0.1 17 | github.com/spf13/cobra v1.0.0 18 | github.com/spf13/pflag v1.0.5 // indirect 19 | github.com/stretchr/testify v1.6.1 20 | github.com/sundowndev/dorkgen v1.2.0 21 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a // indirect 22 | golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527 // indirect 23 | gopkg.in/h2non/gock.v1 v1.0.15 24 | ) 25 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "gopkg.in/sundowndev/phoneinfoga.v2/cmd" 5 | ) 6 | 7 | func main() { 8 | cmd.Execute() 9 | } 10 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: PhoneInfoga 2 | repo_name: 'sundowndev/PhoneInfoga' 3 | repo_url: 'https://github.com/sundowndev/PhoneInfoga' 4 | site_description: 'Advanced information gathering & OSINT tool for phone numbers.' 5 | site_author: 'Sundowndev' 6 | copyright: 'PhoneInfoga was developed by sundowndev and is licensed under GPL-3.0.' 7 | nav: 8 | - 'Welcome': index.md 9 | - 'Installation': install.md 10 | - 'Getting started': usage.md 11 | - 'Formatting phone numbers': formatting.md 12 | - 'Go module usage': go-module-usage.md 13 | - 'Resources': resources.md 14 | - 'Contribute': contribute.md 15 | theme: 16 | name: material 17 | logo: './images/logo_white.svg' 18 | favicon: './images/logo.svg' 19 | palette: 20 | primary: 'blue grey' 21 | accent: 'brown' 22 | extra: 23 | social: 24 | - icon: fontawesome/brands/github-alt 25 | link: 'https://github.com/sundowndev/PhoneInfoga' 26 | - icon: fontawesome/brands/twitter 27 | link: 'https://twitter.com/sundowndev' 28 | 29 | # Extensions 30 | markdown_extensions: 31 | - markdown.extensions.admonition 32 | plugins: 33 | - search 34 | - minify: 35 | minify_html: true 36 | -------------------------------------------------------------------------------- /pkg/config/config_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "regexp" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestConfig(t *testing.T) { 11 | assert := assert.New(t) 12 | 13 | t.Run("Version", func(t *testing.T) { 14 | t.Run("version should be use semver format", func(t *testing.T) { 15 | matched, err := regexp.MatchString(`^v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(-(0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(\.(0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*)?(\+[0-9a-zA-Z-]+(\.[0-9a-zA-Z-]+)*)?$`, Version) 16 | 17 | assert.Equal(matched, true, "should be equal") 18 | assert.Equal(err, nil, "should be equal") 19 | }) 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /pkg/config/version.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | // Version is the tool's version number as a string 4 | var Version string = "v2.3.0" 5 | -------------------------------------------------------------------------------- /pkg/scanners/google.go: -------------------------------------------------------------------------------- 1 | package scanners 2 | 3 | import ( 4 | "github.com/sundowndev/dorkgen" 5 | ) 6 | 7 | // GoogleSearchDork is the common format for dork requests 8 | type GoogleSearchDork struct { 9 | Number string `json:"number"` 10 | Dork string `json:"dork"` 11 | URL string 12 | } 13 | 14 | // GoogleSearchResponse is the output of Google search scanner. 15 | // It contains all dorks created ordered by types. 16 | type GoogleSearchResponse struct { 17 | SocialMedia []*GoogleSearchDork `json:"socialMedia"` 18 | DisposableProviders []*GoogleSearchDork `json:"disposableProviders"` 19 | Reputation []*GoogleSearchDork `json:"reputation"` 20 | Individuals []*GoogleSearchDork `json:"individuals"` 21 | General []*GoogleSearchDork `json:"general"` 22 | } 23 | 24 | func getDisposableProvidersDorks(number *Number) (results []*GoogleSearchDork) { 25 | var dorks = []*dorkgen.GoogleSearch{ 26 | (&dorkgen.GoogleSearch{}). 27 | Site("hs3x.com"). 28 | Intext(number.International), 29 | (&dorkgen.GoogleSearch{}). 30 | Site("receive-sms-now.com"). 31 | Intext(number.International). 32 | Or(). 33 | Intext(number.RawLocal), 34 | (&dorkgen.GoogleSearch{}). 35 | Site("smslisten.com"). 36 | Intext(number.International). 37 | Or(). 38 | Intext(number.RawLocal), 39 | (&dorkgen.GoogleSearch{}). 40 | Site("smsnumbersonline.com"). 41 | Intext(number.International). 42 | Or(). 43 | Intext(number.RawLocal), 44 | (&dorkgen.GoogleSearch{}). 45 | Site("freesmscode.com"). 46 | Intext(number.International). 47 | Or(). 48 | Intext(number.RawLocal), 49 | (&dorkgen.GoogleSearch{}). 50 | Site("catchsms.com"). 51 | Intext(number.International). 52 | Or(). 53 | Intext(number.RawLocal), 54 | (&dorkgen.GoogleSearch{}). 55 | Site("smstibo.com"). 56 | Intext(number.International). 57 | Or(). 58 | Intext(number.RawLocal), 59 | (&dorkgen.GoogleSearch{}). 60 | Site("smsreceiving.com"). 61 | Intext(number.International). 62 | Or(). 63 | Intext(number.RawLocal), 64 | (&dorkgen.GoogleSearch{}). 65 | Site("getfreesmsnumber.com"). 66 | Intext(number.International). 67 | Or(). 68 | Intext(number.RawLocal), 69 | (&dorkgen.GoogleSearch{}). 70 | Site("sellaite.com"). 71 | Intext(number.International). 72 | Or(). 73 | Intext(number.RawLocal), 74 | (&dorkgen.GoogleSearch{}). 75 | Site("receive-sms-online.info"). 76 | Intext(number.International). 77 | Or(). 78 | Intext(number.RawLocal), 79 | (&dorkgen.GoogleSearch{}). 80 | Site("receivesmsonline.com"). 81 | Intext(number.International). 82 | Or(). 83 | Intext(number.RawLocal), 84 | (&dorkgen.GoogleSearch{}). 85 | Site("receive-a-sms.com"). 86 | Intext(number.International). 87 | Or(). 88 | Intext(number.RawLocal), 89 | (&dorkgen.GoogleSearch{}). 90 | Site("sms-receive.net"). 91 | Intext(number.International). 92 | Or(). 93 | Intext(number.RawLocal), 94 | (&dorkgen.GoogleSearch{}). 95 | Site("receivefreesms.com"). 96 | Intext(number.International). 97 | Or(). 98 | Intext(number.RawLocal), 99 | (&dorkgen.GoogleSearch{}). 100 | Site("receive-sms.com"). 101 | Intext(number.International). 102 | Or(). 103 | Intext(number.RawLocal), 104 | (&dorkgen.GoogleSearch{}). 105 | Site("receivetxt.com"). 106 | Intext(number.International). 107 | Or(). 108 | Intext(number.RawLocal), 109 | (&dorkgen.GoogleSearch{}). 110 | Site("freephonenum.com"). 111 | Intext(number.International). 112 | Or(). 113 | Intext(number.RawLocal), 114 | (&dorkgen.GoogleSearch{}). 115 | Site("freesmsverification.com"). 116 | Intext(number.International). 117 | Or(). 118 | Intext(number.RawLocal), 119 | (&dorkgen.GoogleSearch{}). 120 | Site("receive-sms-online.com"). 121 | Intext(number.International). 122 | Or(). 123 | Intext(number.RawLocal), 124 | (&dorkgen.GoogleSearch{}). 125 | Site("smslive.co"). 126 | Intext(number.International). 127 | Or(). 128 | Intext(number.RawLocal), 129 | } 130 | 131 | for _, dork := range dorks { 132 | results = append(results, &GoogleSearchDork{ 133 | Number: number.E164, 134 | Dork: dork.String(), 135 | URL: dork.ToURL(), 136 | }) 137 | } 138 | 139 | return results 140 | } 141 | 142 | func getIndividualsDorks(number *Number, formats ...string) (results []*GoogleSearchDork) { 143 | var dorks = []*dorkgen.GoogleSearch{ 144 | (&dorkgen.GoogleSearch{}). 145 | Site("numinfo.net"). 146 | Intext(number.International). 147 | Or(). 148 | Intext(number.E164). 149 | Or(). 150 | Intext(number.RawLocal), 151 | (&dorkgen.GoogleSearch{}). 152 | Site("sync.me"). 153 | Intext(number.International). 154 | Or(). 155 | Intext(number.E164). 156 | Or(). 157 | Intext(number.RawLocal), 158 | (&dorkgen.GoogleSearch{}). 159 | Site("whocallsyou.de"). 160 | Intext(number.RawLocal), 161 | (&dorkgen.GoogleSearch{}). 162 | Site("pastebin.com"). 163 | Intext(number.International). 164 | Or(). 165 | Intext(number.E164). 166 | Or(). 167 | Intext(number.RawLocal), 168 | (&dorkgen.GoogleSearch{}). 169 | Site("whycall.me"). 170 | Intext(number.International). 171 | Or(). 172 | Intext(number.E164). 173 | Or(). 174 | Intext(number.RawLocal), 175 | (&dorkgen.GoogleSearch{}). 176 | Site("locatefamily.com"). 177 | Intext(number.International). 178 | Or(). 179 | Intext(number.E164). 180 | Or(). 181 | Intext(number.RawLocal), 182 | (&dorkgen.GoogleSearch{}). 183 | Site("spytox.com"). 184 | Intext(number.RawLocal), 185 | } 186 | 187 | for _, dork := range dorks { 188 | for _, f := range formats { 189 | dork.Or().Intext(f) 190 | } 191 | 192 | results = append(results, &GoogleSearchDork{ 193 | Number: number.E164, 194 | Dork: dork.String(), 195 | URL: dork.ToURL(), 196 | }) 197 | } 198 | 199 | return results 200 | } 201 | 202 | func getSocialMediaDorks(number *Number, formats ...string) (results []*GoogleSearchDork) { 203 | var dorks = []*dorkgen.GoogleSearch{ 204 | (&dorkgen.GoogleSearch{}). 205 | Site("facebook.com"). 206 | Intext(number.International). 207 | Or(). 208 | Intext(number.E164). 209 | Or(). 210 | Intext(number.RawLocal), 211 | (&dorkgen.GoogleSearch{}). 212 | Site("twitter.com"). 213 | Intext(number.International). 214 | Or(). 215 | Intext(number.E164). 216 | Or(). 217 | Intext(number.RawLocal), 218 | (&dorkgen.GoogleSearch{}). 219 | Site("linkedin.com"). 220 | Intext(number.International). 221 | Or(). 222 | Intext(number.E164). 223 | Or(). 224 | Intext(number.RawLocal), 225 | (&dorkgen.GoogleSearch{}). 226 | Site("instagram.com"). 227 | Intext(number.International). 228 | Or(). 229 | Intext(number.E164). 230 | Or(). 231 | Intext(number.RawLocal), 232 | (&dorkgen.GoogleSearch{}). 233 | Site("vk.com"). 234 | Intext(number.International). 235 | Or(). 236 | Intext(number.E164). 237 | Or(). 238 | Intext(number.RawLocal), 239 | } 240 | 241 | for _, dork := range dorks { 242 | for _, f := range formats { 243 | dork.Or().Intext(f) 244 | } 245 | 246 | results = append(results, &GoogleSearchDork{ 247 | Number: number.E164, 248 | Dork: dork.String(), 249 | URL: dork.ToURL(), 250 | }) 251 | } 252 | 253 | return results 254 | } 255 | 256 | func getReputationDorks(number *Number, formats ...string) (results []*GoogleSearchDork) { 257 | var dorks = []*dorkgen.GoogleSearch{ 258 | (&dorkgen.GoogleSearch{}). 259 | Site("whosenumber.info"). 260 | Intext(number.E164). 261 | Intitle("who called"), 262 | (&dorkgen.GoogleSearch{}). 263 | Intitle("Phone Fraud"). 264 | Intext(number.International). 265 | Or(). 266 | Intext(number.E164). 267 | Or(). 268 | Intext(number.RawLocal), 269 | (&dorkgen.GoogleSearch{}). 270 | Site("findwhocallsme.com"). 271 | Intext(number.E164). 272 | Or(). 273 | Intext(number.International), 274 | (&dorkgen.GoogleSearch{}). 275 | Site("yellowpages.ca"). 276 | Intext(number.E164), 277 | (&dorkgen.GoogleSearch{}). 278 | Site("phonenumbers.ie"). 279 | Intext(number.E164), 280 | (&dorkgen.GoogleSearch{}). 281 | Site("who-calledme.com"). 282 | Intext(number.E164), 283 | (&dorkgen.GoogleSearch{}). 284 | Site("usphonesearch.net"). 285 | Intext(number.RawLocal), 286 | (&dorkgen.GoogleSearch{}). 287 | Site("whocalled.us"). 288 | Inurl(number.RawLocal), 289 | (&dorkgen.GoogleSearch{}). 290 | Site("quinumero.info"). 291 | Intext(number.RawLocal). 292 | Or(). 293 | Intext(number.International), 294 | (&dorkgen.GoogleSearch{}). 295 | Site("uk.popularphotolook.com"). 296 | Inurl(number.RawLocal), 297 | } 298 | 299 | for _, dork := range dorks { 300 | for _, f := range formats { 301 | dork.Or().Intext(f) 302 | } 303 | 304 | results = append(results, &GoogleSearchDork{ 305 | Number: number.E164, 306 | Dork: dork.String(), 307 | URL: dork.ToURL(), 308 | }) 309 | } 310 | 311 | return results 312 | } 313 | 314 | func getGeneralDorks(number *Number, formats ...string) (results []*GoogleSearchDork) { 315 | var dorks = []*dorkgen.GoogleSearch{ 316 | (&dorkgen.GoogleSearch{}). 317 | Intext(number.International). 318 | Or(). 319 | Intext(number.E164). 320 | Or(). 321 | Intext(number.RawLocal). 322 | Or(). 323 | Intext(number.Local), 324 | (&dorkgen.GoogleSearch{}). 325 | Group((&dorkgen.GoogleSearch{}). 326 | Ext("doc"). 327 | Or(). 328 | Ext("docx"). 329 | Or(). 330 | Ext("odt"). 331 | Or(). 332 | Ext("pdf"). 333 | Or(). 334 | Ext("rtf"). 335 | Or(). 336 | Ext("sxw"). 337 | Or(). 338 | Ext("psw"). 339 | Or(). 340 | Ext("ppt"). 341 | Or(). 342 | Ext("pptx"). 343 | Or(). 344 | Ext("pps"). 345 | Or(). 346 | Ext("csv"). 347 | Or(). 348 | Ext("txt"). 349 | Or(). 350 | Ext("xls")). 351 | Intext(number.International). 352 | Or(). 353 | Intext(number.E164). 354 | Or(). 355 | Intext(number.RawLocal), 356 | } 357 | 358 | for _, dork := range dorks { 359 | for _, f := range formats { 360 | dork.Or().Intext(f) 361 | } 362 | 363 | results = append(results, &GoogleSearchDork{ 364 | Number: number.E164, 365 | Dork: dork.String(), 366 | URL: dork.ToURL(), 367 | }) 368 | } 369 | 370 | return results 371 | } 372 | 373 | // GoogleSearchScan creates several Google requests to search footprints of 374 | // the number online through Google search. 375 | func GoogleSearchScan(number *Number, formats ...string) (results GoogleSearchResponse) { 376 | results.SocialMedia = getSocialMediaDorks(number, formats...) 377 | results.Reputation = getReputationDorks(number, formats...) 378 | results.Individuals = getIndividualsDorks(number, formats...) 379 | results.DisposableProviders = getDisposableProvidersDorks(number) 380 | results.General = getGeneralDorks(number, formats...) 381 | 382 | return results 383 | } 384 | -------------------------------------------------------------------------------- /pkg/scanners/local.go: -------------------------------------------------------------------------------- 1 | package scanners 2 | 3 | import ( 4 | "github.com/nyaruka/phonenumbers" 5 | "gopkg.in/sundowndev/phoneinfoga.v2/pkg/utils" 6 | ) 7 | 8 | // LocalScan performs a local scan of a phone 9 | // number using phonenumbers library 10 | func LocalScan(number string) (res *Number, err error) { 11 | n := "+" + utils.FormatNumber(number) 12 | country := utils.ParseCountryCode(n) 13 | 14 | num, err := phonenumbers.Parse(n, country) 15 | 16 | if err != nil { 17 | return nil, err 18 | } 19 | 20 | res = &Number{ 21 | RawLocal: utils.FormatNumber(phonenumbers.Format(num, phonenumbers.NATIONAL)), 22 | Local: phonenumbers.Format(num, phonenumbers.NATIONAL), 23 | E164: phonenumbers.Format(num, phonenumbers.E164), 24 | International: utils.FormatNumber(phonenumbers.Format(num, phonenumbers.E164)), 25 | CountryCode: num.GetCountryCode(), 26 | Country: country, 27 | Carrier: num.GetPreferredDomesticCarrierCode(), 28 | } 29 | 30 | return res, nil 31 | } 32 | -------------------------------------------------------------------------------- /pkg/scanners/local_test.go: -------------------------------------------------------------------------------- 1 | package scanners 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "gopkg.in/sundowndev/phoneinfoga.v2/pkg/utils" 8 | ) 9 | 10 | func TestLocalScan(t *testing.T) { 11 | assert := assert.New(t) 12 | 13 | t.Run("should scan number", func(t *testing.T) { 14 | result, err := localScanCLI(utils.LoggerService, "+1 718-521-2994") 15 | 16 | expectedResult := &Number{ 17 | RawLocal: "7185212994", 18 | Local: "(718) 521-2994", 19 | E164: "+17185212994", 20 | International: "17185212994", 21 | CountryCode: 1, 22 | Country: "US", 23 | Carrier: "", 24 | } 25 | 26 | assert.Equal(expectedResult, result, "they should be equal") 27 | assert.Nil(err, "they should be equal") 28 | }) 29 | 30 | t.Run("should fail and return error", func(t *testing.T) { 31 | _, err := LocalScan("this is not a phone number") 32 | 33 | assert.Equal(err.Error(), "the phone number supplied is not a number", "they should be equal") 34 | }) 35 | } 36 | -------------------------------------------------------------------------------- /pkg/scanners/numverify.go: -------------------------------------------------------------------------------- 1 | package scanners 2 | 3 | import ( 4 | "crypto/md5" 5 | "encoding/hex" 6 | "encoding/json" 7 | "fmt" 8 | "net/http" 9 | 10 | "github.com/PuerkitoBio/goquery" 11 | ) 12 | 13 | // NumverifyScannerResponse REST API response 14 | type NumverifyScannerResponse struct { 15 | Valid bool `json:"valid"` 16 | Number string `json:"number"` 17 | LocalFormat string `json:"local_format"` 18 | InternationalFormat string `json:"international_format"` 19 | CountryPrefix string `json:"country_prefix"` 20 | CountryCode string `json:"country_code"` 21 | CountryName string `json:"country_name"` 22 | Location string `json:"location"` 23 | Carrier string `json:"carrier"` 24 | LineType string `json:"line_type"` 25 | } 26 | 27 | // NumverifyScan fetches Numverify's API 28 | func NumverifyScan(number *Number) (res *NumverifyScannerResponse, err error) { 29 | html, err := http.Get("http://numverify.com/") 30 | if err != nil { 31 | return nil, err 32 | } 33 | defer html.Body.Close() 34 | 35 | // Load the HTML document 36 | doc, err := goquery.NewDocumentFromReader(html.Body) 37 | if err != nil { 38 | return nil, err 39 | } 40 | 41 | secret, _ := doc.Find("[name=\"scl_request_secret\"]").Attr("value") 42 | 43 | // Then fetch REST API 44 | safeNumber := number.International 45 | apiKey := md5.Sum([]byte(safeNumber + secret)) 46 | 47 | url := fmt.Sprintf("https://numverify.com/php_helper_scripts/phone_api.php?secret_key=%s&number=%s", hex.EncodeToString(apiKey[:]), safeNumber) 48 | 49 | // Build the request 50 | response, err := http.Get(url) 51 | if err != nil { 52 | return nil, err 53 | } 54 | defer response.Body.Close() 55 | 56 | // Fill the response with the data from the JSON 57 | var result NumverifyScannerResponse 58 | 59 | // Use json.Decode for reading streams of JSON data 60 | if err := json.NewDecoder(response.Body).Decode(&result); err != nil { 61 | return nil, err 62 | } 63 | 64 | res = &NumverifyScannerResponse{ 65 | Valid: result.Valid, 66 | Number: result.Number, 67 | LocalFormat: result.LocalFormat, 68 | InternationalFormat: result.InternationalFormat, 69 | CountryPrefix: result.CountryPrefix, 70 | CountryCode: result.CountryCode, 71 | CountryName: result.CountryName, 72 | Location: result.Location, 73 | Carrier: result.Carrier, 74 | LineType: result.LineType, 75 | } 76 | 77 | return res, nil 78 | } 79 | -------------------------------------------------------------------------------- /pkg/scanners/numverify_test.go: -------------------------------------------------------------------------------- 1 | package scanners 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | gock "gopkg.in/h2non/gock.v1" 8 | "gopkg.in/sundowndev/phoneinfoga.v2/pkg/utils" 9 | ) 10 | 11 | func TestNumverifyScanner(t *testing.T) { 12 | assert := assert.New(t) 13 | 14 | t.Run("should succeed", func(t *testing.T) { 15 | defer gock.Off() // Flush pending mocks after test execution 16 | 17 | expectedResult := NumverifyScannerResponse{ 18 | Valid: true, 19 | Number: "79516566591", 20 | LocalFormat: "9516566591", 21 | InternationalFormat: "+79516566591", 22 | CountryPrefix: "+7", 23 | CountryCode: "RU", 24 | CountryName: "Russian Federation", 25 | Location: "Saint Petersburg and Leningrad Oblast", 26 | Carrier: "OJSC St. Petersburg Telecom (OJSC Tele2-Saint-Petersburg)", 27 | LineType: "mobile", 28 | } 29 | 30 | number, _ := LocalScan("+79516566591") 31 | 32 | gock.New("http://numverify.com"). 33 | Get("/"). 34 | Reply(200).BodyString(``) 35 | 36 | gock.New("https://numverify.com"). 37 | Get("/php_helper_scripts/phone_api.php"). 38 | MatchParam("secret_key", "5ad5554ac240e4d3d31107941b35a5eb"). 39 | MatchParam("number", number.International). 40 | Reply(200). 41 | JSON(expectedResult) 42 | 43 | result, err := numverifyScanCLI(utils.LoggerService, number) 44 | 45 | assert.Nil(err, "they should be equal") 46 | assert.Equal(result, &expectedResult, "they should be equal") 47 | 48 | assert.Equal(gock.IsDone(), true, "there should have no pending mocks") 49 | }) 50 | 51 | t.Run("should return invalid number", func(t *testing.T) { 52 | defer gock.Off() // Flush pending mocks after test execution 53 | 54 | expectedResult := NumverifyScannerResponse{ 55 | Valid: false, 56 | Number: "", 57 | LocalFormat: "", 58 | InternationalFormat: "", 59 | CountryPrefix: "", 60 | CountryCode: "", 61 | CountryName: "", 62 | Location: "", 63 | Carrier: "", 64 | LineType: "", 65 | } 66 | 67 | number, _ := LocalScan("+123456789") 68 | 69 | gock.New("http://numverify.com"). 70 | Get("/"). 71 | Reply(200).BodyString(``) 72 | 73 | gock.New("https://numverify.com"). 74 | Get("/php_helper_scripts/phone_api.php"). 75 | MatchParam("secret_key", "7ccde16e862dfe7681297713e9e9cadb"). 76 | MatchParam("number", number.International). 77 | Reply(200). 78 | JSON(expectedResult) 79 | 80 | result, err := numverifyScanCLI(utils.LoggerService, number) 81 | 82 | assert.Nil(err, "they should be equal") 83 | assert.Equal(result, &expectedResult, "they should be equal") 84 | 85 | assert.Equal(gock.IsDone(), true, "there should have no pending mocks") 86 | }) 87 | 88 | t.Run("should return empty response and handle error properly", func(t *testing.T) { 89 | defer gock.Off() // Flush pending mocks after test execution 90 | 91 | number, _ := LocalScan("+123456789") 92 | 93 | gock.New("http://numverify.com"). 94 | Get("/"). 95 | Reply(200).BodyString(``) 96 | 97 | gock.New("https://numverify.com"). 98 | Get("/php_helper_scripts/phone_api.php"). 99 | MatchParam("secret_key", "7ccde16e862dfe7681297713e9e9cadb"). 100 | MatchParam("number", number.International). 101 | Reply(200) 102 | 103 | result, err := numverifyScanCLI(utils.LoggerService, number) 104 | 105 | assert.EqualError(err, "EOF", "they should be equal") 106 | assert.Nil(result, "they should be equal") 107 | 108 | assert.Equal(gock.IsDone(), true, "there should have no pending mocks") 109 | }) 110 | } 111 | -------------------------------------------------------------------------------- /pkg/scanners/ovh.go: -------------------------------------------------------------------------------- 1 | package scanners 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "reflect" 8 | "strings" 9 | ) 10 | 11 | // OVHAPIResponseNumber is a type that describes an OVH number range 12 | type OVHAPIResponseNumber struct { 13 | MatchingCriteria interface{} `json:"matchingCriteria"` 14 | City string `json:"city"` 15 | ZneList []string `json:"zneList"` 16 | InternationalNumber string `json:"internationalNumber"` 17 | Country string `json:"country"` 18 | AskedCity interface{} `json:"askedCity"` 19 | ZipCode string `json:"zipCode"` 20 | Number string `json:"number"` 21 | Prefix int `json:"prefix"` 22 | } 23 | 24 | // OVHScannerResponse is the OVH scanner response 25 | type OVHScannerResponse struct { 26 | Found bool `json:"found"` 27 | NumberRange string `json:"numberRange"` 28 | City string `json:"city"` 29 | ZipCode string `json:"zipCode"` 30 | } 31 | 32 | // OVHScan fetches OVH's REST API 33 | func OVHScan(number *Number) (res *OVHScannerResponse, err error) { 34 | countryCode := strings.ToLower(number.Country) 35 | url := fmt.Sprintf("https://api.ovh.com/1.0/telephony/number/detailedZones?country=%s", countryCode) 36 | 37 | // Build the request 38 | response, err := http.Get(url) 39 | if err != nil { 40 | return nil, err 41 | } 42 | defer response.Body.Close() 43 | 44 | // Fill the response with the data from the JSON 45 | var results []OVHAPIResponseNumber 46 | 47 | // Use json.Decode for reading streams of JSON data 48 | json.NewDecoder(response.Body).Decode(&results) 49 | 50 | var foundNumber OVHAPIResponseNumber 51 | 52 | rt := reflect.TypeOf(results) 53 | if rt.Kind() == reflect.Slice && len(number.RawLocal) > 6 { 54 | askedNumber := number.RawLocal[0:6] + "xxxx" 55 | 56 | for _, result := range results { 57 | if result.Number == askedNumber { 58 | foundNumber = result 59 | } 60 | } 61 | } 62 | 63 | res = &OVHScannerResponse{ 64 | Found: len(foundNumber.Number) > 0, 65 | NumberRange: foundNumber.Number, 66 | City: foundNumber.City, 67 | ZipCode: foundNumber.ZipCode, 68 | } 69 | 70 | return res, nil 71 | } 72 | -------------------------------------------------------------------------------- /pkg/scanners/ovh_test.go: -------------------------------------------------------------------------------- 1 | package scanners 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | gock "gopkg.in/h2non/gock.v1" 8 | "gopkg.in/sundowndev/phoneinfoga.v2/pkg/utils" 9 | ) 10 | 11 | func TestOVHScanner(t *testing.T) { 12 | assert := assert.New(t) 13 | 14 | t.Run("should find number on OVH", func(t *testing.T) { 15 | defer gock.Off() // Flush pending mocks after test execution 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 | number, _ := LocalScan("+33 0365179268") 36 | 37 | result, err := ovhScanCLI(utils.LoggerService, number) 38 | 39 | assert.Equal(result, &OVHScannerResponse{ 40 | Found: true, 41 | NumberRange: "036517xxxx", 42 | City: "Abbeville", 43 | ZipCode: "", 44 | }, "they should be equal") 45 | 46 | assert.Equal(gock.IsDone(), true, "there should have no pending mocks") 47 | assert.Nil(err, "they should be equal") 48 | }) 49 | 50 | t.Run("should not find number on OVH", func(t *testing.T) { 51 | defer gock.Off() // Flush pending mocks after test execution 52 | 53 | gock.New("https://api.ovh.com"). 54 | Get("/1.0/telephony/number/detailedZones"). 55 | MatchParam("country", "us"). 56 | Reply(200). 57 | JSON([]OVHAPIResponseNumber{ 58 | { 59 | ZneList: []string{}, 60 | MatchingCriteria: "", 61 | Prefix: 33, 62 | InternationalNumber: "003336517xxxx", 63 | Country: "fr", 64 | ZipCode: "", 65 | Number: "036517xxxx", 66 | City: "Abbeville", 67 | AskedCity: "", 68 | }, 69 | }) 70 | 71 | number, _ := LocalScan("+1 718-521-2994") 72 | 73 | result, err := OVHScan(number) 74 | 75 | assert.Equal(err, nil, "should not be errored") 76 | assert.Equal(result, &OVHScannerResponse{ 77 | Found: false, 78 | NumberRange: "", 79 | City: "", 80 | ZipCode: "", 81 | }, "they should be equal") 82 | 83 | assert.Equal(gock.IsDone(), true, "there should have no pending mocks") 84 | }) 85 | 86 | t.Run("should not find country code on OVH", func(t *testing.T) { 87 | defer gock.Off() // Flush pending mocks after test execution 88 | 89 | gock.New("https://api.ovh.com"). 90 | Get("/1.0/telephony/number/detailedZones"). 91 | MatchParam("country", "us"). 92 | Reply(200). 93 | JSON(map[string]string{ 94 | "message": "[country] Given data (us) does not belong to the NumberCountryEnum enumeration", 95 | }) 96 | 97 | number, _ := LocalScan("+1 718-521-2994") 98 | 99 | result, err := OVHScan(number) 100 | 101 | assert.Equal(err, nil, "should not be errored") 102 | assert.Equal(result, &OVHScannerResponse{ 103 | Found: false, 104 | NumberRange: "", 105 | City: "", 106 | ZipCode: "", 107 | }, "they should be equal") 108 | 109 | assert.Equal(gock.IsDone(), true, "there should have no pending mocks") 110 | }) 111 | } 112 | -------------------------------------------------------------------------------- /pkg/scanners/scanners.go: -------------------------------------------------------------------------------- 1 | package scanners 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | "gopkg.in/sundowndev/phoneinfoga.v2/pkg/utils" 8 | ) 9 | 10 | // Number is a phone number 11 | type Number struct { 12 | RawLocal string `json:"rawLocal"` 13 | Local string `json:"local"` 14 | E164 string `json:"E164"` 15 | International string `json:"international"` 16 | CountryCode int32 `json:"countryCode"` 17 | Country string `json:"country"` 18 | Carrier string `json:"carrier"` 19 | } 20 | 21 | func localScanCLI(l *utils.Logger, number string) (*Number, error) { 22 | l.Infoln("Running local scan...") 23 | 24 | scan, err := LocalScan(number) 25 | 26 | if err != nil { 27 | l.Errorln("An error occurred:", err.Error()) 28 | return nil, err 29 | } 30 | 31 | l.Successln("Local format:", scan.Local) 32 | l.Successln("E164 format:", scan.E164) 33 | l.Successln("International format:", scan.International) 34 | l.Successf("Country found: +%v (%v)", scan.CountryCode, scan.Country) 35 | l.Successln("Carrier:", scan.Carrier) 36 | 37 | return scan, nil 38 | } 39 | 40 | func numverifyScanCLI(l *utils.Logger, number *Number) (*NumverifyScannerResponse, error) { 41 | l.Infoln("Running Numverify.com scan...") 42 | 43 | scan, err := NumverifyScan(number) 44 | 45 | if err != nil { 46 | l.Errorln("An error occurred:", err.Error()) 47 | l.Errorln("It may be Numverify not finding information about that number, or the number being invalid.") 48 | return nil, err 49 | } 50 | 51 | l.Successf(`Valid: %v`, scan.Valid) 52 | l.Successln("Number:", scan.Number) 53 | l.Successln("Local format:", scan.LocalFormat) 54 | l.Successln("International format:", scan.InternationalFormat) 55 | l.Successf("Country code: %v (%v)", scan.CountryCode, scan.CountryPrefix) 56 | l.Successln("Country:", scan.CountryName) 57 | l.Successln("Location:", scan.Location) 58 | l.Successln("Carrier:", scan.Carrier) 59 | l.Successln("Line type:", scan.LineType) 60 | 61 | return scan, nil 62 | } 63 | 64 | func googlesearchScanCLI(l *utils.Logger, number *Number, formats ...string) GoogleSearchResponse { 65 | l.Infoln("Generating Google search dork requests...") 66 | 67 | scan := GoogleSearchScan(number, formats...) 68 | 69 | l.Infoln("Social media footprints") 70 | for _, dork := range scan.SocialMedia { 71 | l.Successf(`Link: %v`, dork.URL) 72 | } 73 | 74 | l.Infoln("Individual footprints") 75 | for _, dork := range scan.Individuals { 76 | l.Successf(`Link: %v`, dork.URL) 77 | } 78 | 79 | l.Infoln("Reputation footprints") 80 | for _, dork := range scan.Reputation { 81 | l.Successf(`Link: %v`, dork.URL) 82 | } 83 | 84 | l.Infoln("Temporary number providers footprints") 85 | for _, dork := range scan.DisposableProviders { 86 | l.Successf(`Link: %v`, dork.URL) 87 | } 88 | 89 | return scan 90 | } 91 | 92 | func ovhScanCLI(l *utils.Logger, number *Number) (*OVHScannerResponse, error) { 93 | l.Infoln("Running OVH API scan...") 94 | 95 | scan, err := OVHScan(number) 96 | 97 | if err != nil { 98 | l.Errorln("An error occurred:", err.Error()) 99 | return nil, err 100 | } 101 | 102 | l.Successf(`Found: %v`, scan.Found) 103 | l.Successf(`Number range: %v`, scan.NumberRange) 104 | l.Successln("City:", scan.City) 105 | l.Successln("Zip code:", scan.ZipCode) 106 | 107 | return scan, nil 108 | } 109 | 110 | // ScanCLI Run scans with CLI output 111 | func ScanCLI(number string) { 112 | num, err := localScanCLI(utils.LoggerService, number) 113 | 114 | if err != nil { 115 | log.Fatal(err) 116 | os.Exit(1) 117 | } 118 | 119 | numverifyScanCLI(utils.LoggerService, num) 120 | googlesearchScanCLI(utils.LoggerService, num) 121 | ovhScanCLI(utils.LoggerService, num) 122 | } 123 | -------------------------------------------------------------------------------- /pkg/utils/logger.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/fatih/color" 7 | ) 8 | 9 | // Logger allows you to log messages in the terminal 10 | type Logger struct { 11 | NewColor func(value ...color.Attribute) colorLogger 12 | } 13 | 14 | type colorLogger interface { 15 | Println(a ...interface{}) (int, error) 16 | Printf(format string, a ...interface{}) (int, error) 17 | } 18 | 19 | // Infoln logs an info message 20 | func (l *Logger) Infoln(s ...string) { 21 | l.NewColor(color.FgCyan).Println("[i]", strings.Join(s, " ")) 22 | } 23 | 24 | // Warnln logs an warning message 25 | func (l *Logger) Warnln(s ...string) { 26 | l.NewColor(color.FgYellow).Println("[*]", strings.Join(s, " ")) 27 | } 28 | 29 | // Errorln logs an error message 30 | func (l *Logger) Errorln(s ...string) { 31 | l.NewColor(color.FgRed).Println("[!]", strings.Join(s, " ")) 32 | } 33 | 34 | // Successln logs a success message 35 | func (l *Logger) Successln(s ...string) { 36 | l.NewColor(color.FgGreen).Println("[+]", strings.Join(s, " ")) 37 | } 38 | 39 | // Successf logs a success message 40 | func (l *Logger) Successf(format string, messages ...interface{}) { 41 | l.NewColor(color.FgGreen).Printf("[+] "+format, messages...) 42 | l.NewColor().Printf("\n") 43 | } 44 | 45 | // LoggerService is the default logger instance 46 | var LoggerService = &Logger{ 47 | NewColor: func(value ...color.Attribute) colorLogger { 48 | return color.New(value...) 49 | }, 50 | } 51 | -------------------------------------------------------------------------------- /pkg/utils/mocks/Color.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v1.0.0. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import mock "github.com/stretchr/testify/mock" 6 | 7 | // Color is an autogenerated mock type for the Color type 8 | type Color struct { 9 | mock.Mock 10 | } 11 | 12 | // Printf provides a mock function with given fields: format, a 13 | func (_m *Color) Printf(format string, a ...interface{}) (int, error) { 14 | var _ca []interface{} 15 | _ca = append(_ca, format) 16 | _ca = append(_ca, a...) 17 | ret := _m.Called(_ca...) 18 | 19 | var r0 int 20 | if rf, ok := ret.Get(0).(func(string, ...interface{}) int); ok { 21 | r0 = rf(format, a...) 22 | } else { 23 | r0 = ret.Get(0).(int) 24 | } 25 | 26 | var r1 error 27 | if rf, ok := ret.Get(1).(func(string, ...interface{}) error); ok { 28 | r1 = rf(format, a...) 29 | } else { 30 | r1 = ret.Error(1) 31 | } 32 | 33 | return r0, r1 34 | } 35 | 36 | // Println provides a mock function with given fields: a 37 | func (_m *Color) Println(a ...interface{}) (int, error) { 38 | var _ca []interface{} 39 | _ca = append(_ca, a...) 40 | ret := _m.Called(_ca...) 41 | 42 | var r0 int 43 | if rf, ok := ret.Get(0).(func(...interface{}) int); ok { 44 | r0 = rf(a...) 45 | } else { 46 | r0 = ret.Get(0).(int) 47 | } 48 | 49 | var r1 error 50 | if rf, ok := ret.Get(1).(func(...interface{}) error); ok { 51 | r1 = rf(a...) 52 | } else { 53 | r1 = ret.Error(1) 54 | } 55 | 56 | return r0, r1 57 | } 58 | -------------------------------------------------------------------------------- /pkg/utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 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]+$") 33 | 34 | return len(re.FindString(number)) != 0 35 | } 36 | -------------------------------------------------------------------------------- /pkg/utils/utils_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/fatih/color" 7 | "github.com/stretchr/testify/assert" 8 | "gopkg.in/sundowndev/phoneinfoga.v2/pkg/utils/mocks" 9 | ) 10 | 11 | func TestUtils(t *testing.T) { 12 | assert := assert.New(t) 13 | 14 | t.Run("FormatNumber", func(t *testing.T) { 15 | t.Run("should format number correctly", func(t *testing.T) { 16 | result := FormatNumber("+1 555-444-2222") 17 | 18 | assert.Equal(result, "15554442222", "they should be equal") 19 | }) 20 | 21 | t.Run("should format number correctly", func(t *testing.T) { 22 | result := FormatNumber("+1 (315) 284-1580") 23 | 24 | assert.Equal(result, "13152841580", "they should be equal") 25 | }) 26 | }) 27 | 28 | t.Run("ParseCountryCode", func(t *testing.T) { 29 | t.Run("should parse country code correctly", func(t *testing.T) { 30 | result := ParseCountryCode("+33 679368229") 31 | 32 | assert.Equal(result, "FR", "they should be equal") 33 | }) 34 | 35 | t.Run("should parse country code correctly", func(t *testing.T) { 36 | result := ParseCountryCode("+1 315-284-1580") 37 | 38 | assert.Equal(result, "US", "they should be equal") 39 | }) 40 | 41 | t.Run("should parse country code correctly", func(t *testing.T) { 42 | result := ParseCountryCode("4566118311") 43 | 44 | assert.Equal(result, "DK", "they should be equal") 45 | }) 46 | }) 47 | 48 | t.Run("IsValid", func(t *testing.T) { 49 | t.Run("should validate phone number", func(t *testing.T) { 50 | result := IsValid("+1 315-284-1580") 51 | 52 | assert.Equal(result, true, "they should be equal") 53 | }) 54 | 55 | t.Run("should validate phone number", func(t *testing.T) { 56 | result := IsValid("P+1 315-284-1580A") 57 | 58 | assert.Equal(result, false, "they should be equal") 59 | }) 60 | }) 61 | 62 | t.Run("Logger", func(t *testing.T) { 63 | t.Run("Infoln", func(t *testing.T) { 64 | mLogger := new(mocks.Color) 65 | 66 | mLogger.On("Println", "[i]", "test").Once().Return(0, nil) 67 | 68 | log := &Logger{ 69 | NewColor: func(value ...color.Attribute) colorLogger { 70 | assert.Equal([]color.Attribute([]color.Attribute{36}), value, "they should be equal") 71 | 72 | return mLogger 73 | }, 74 | } 75 | 76 | log.Infoln("test") 77 | 78 | mLogger.AssertExpectations(t) 79 | }) 80 | 81 | t.Run("Warnln", func(t *testing.T) { 82 | mLogger := new(mocks.Color) 83 | 84 | mLogger.On("Println", "[*]", "test").Once().Return(0, nil) 85 | 86 | log := &Logger{ 87 | NewColor: func(value ...color.Attribute) colorLogger { 88 | assert.Equal([]color.Attribute([]color.Attribute{33}), value, "they should be equal") 89 | 90 | return mLogger 91 | }, 92 | } 93 | 94 | log.Warnln("test") 95 | 96 | mLogger.AssertExpectations(t) 97 | }) 98 | 99 | t.Run("Errorln", func(t *testing.T) { 100 | mLogger := new(mocks.Color) 101 | 102 | mLogger.On("Println", "[!]", "test").Once().Return(0, nil) 103 | 104 | log := &Logger{ 105 | NewColor: func(value ...color.Attribute) colorLogger { 106 | assert.Equal([]color.Attribute([]color.Attribute{31}), value, "they should be equal") 107 | 108 | return mLogger 109 | }, 110 | } 111 | 112 | log.Errorln("test") 113 | 114 | mLogger.AssertExpectations(t) 115 | }) 116 | 117 | t.Run("Successln", func(t *testing.T) { 118 | mLogger := new(mocks.Color) 119 | 120 | mLogger.On("Println", "[+]", "test").Once().Return(0, nil) 121 | 122 | log := &Logger{ 123 | NewColor: func(value ...color.Attribute) colorLogger { 124 | assert.Equal([]color.Attribute([]color.Attribute{32}), value, "they should be equal") 125 | 126 | return mLogger 127 | }, 128 | } 129 | 130 | log.Successln("test") 131 | 132 | mLogger.AssertExpectations(t) 133 | }) 134 | 135 | t.Run("Successf", func(t *testing.T) { 136 | var ColorNumberOfCalls int 137 | 138 | mLogger := new(mocks.Color) 139 | 140 | mLogger.On("Printf", "[+] %s", "test").Once().Return(0, nil) 141 | mLogger.On("Printf", "\n").Once().Return(0, nil) 142 | 143 | log := &Logger{ 144 | NewColor: func(value ...color.Attribute) colorLogger { 145 | if ColorNumberOfCalls == 0 { 146 | assert.Equal([]color.Attribute([]color.Attribute{32}), value, "they should be equal") 147 | ColorNumberOfCalls++ 148 | } 149 | 150 | return mLogger 151 | }, 152 | } 153 | 154 | log.Successf("%s", "test") 155 | 156 | mLogger.AssertNumberOfCalls(t, "Printf", 2) 157 | mLogger.AssertExpectations(t) 158 | }) 159 | }) 160 | } 161 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ], 5 | "labels": [ 6 | "dependencies", 7 | "renovate" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /scripts/docker_push.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin sh 2 | 3 | # This script is used to push the image to Docker hub 4 | docker login 5 | docker build --rm=true -t sundowndev/phoneinfoga:latest . 6 | docker tag $(docker images -q sundowndev/phoneinfoga) sundowndev/phoneinfoga:latest 7 | docker push sundowndev/phoneinfoga 8 | 9 | echo 'Script executed.' -------------------------------------------------------------------------------- /scripts/format.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "Formatting Go files..." 4 | 5 | gofmt -s -w -l . 6 | 7 | echo "Done." 8 | --------------------------------------------------------------------------------