├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── install-check.yml │ └── release.yaml ├── .gitignore ├── INSTALL.md ├── LICENSE ├── README.md ├── cmd ├── brute.go ├── recon.go ├── report.go ├── report_convert.go ├── report_elastic.go ├── resolve.go ├── resolve_bloodhound.go ├── resolve_crtsh.go ├── resolve_file.go ├── root.go └── version.go ├── go.mod ├── go.sum ├── internal ├── ascii │ ├── ansi.go │ ├── colors_other.go │ ├── colors_windows.go │ ├── console.go │ ├── console_windows.go │ ├── logo.go │ ├── markdown.go │ ├── spinner.go │ └── spinner_windows.go ├── disk │ ├── disk.go │ ├── disk_bsd.go │ ├── disk_unix.go │ └── disk_windows.go ├── socks_dns_client.go ├── tools │ ├── fs.go │ ├── hamming.go │ ├── nameserver_others.go │ ├── nameserver_windows.go │ ├── net.go │ ├── slices.go │ ├── string.go │ └── time.go └── version │ └── version.go ├── main.go └── pkg ├── database └── db.go ├── log └── log.go ├── models └── models.go ├── readers ├── crtsh.go ├── file.go ├── file_test.go └── reader.go ├── runner ├── options.go ├── products.go ├── recon.go └── runner.go └── writers ├── csv.go ├── db.go ├── elastic.go ├── json.go ├── memory.go ├── none.go ├── stdout.go ├── text.go └── writer.go /.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 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Version Information:** 27 | - OS: [e.g. iOS] 28 | - enumdns: [e.g. 1.3.3] 29 | 30 | **Additional context** 31 | Add any other context about the problem here. 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/install-check.yml: -------------------------------------------------------------------------------- 1 | name: Go Install Check 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | paths-ignore: 7 | - .gitignore 8 | - README.md 9 | - LICENSE 10 | - TODO 11 | - Dockerfile 12 | 13 | pull_request: 14 | branches: [main] 15 | paths-ignore: 16 | - .gitignore 17 | - README.md 18 | - LICENSE 19 | - TODO 20 | - Dockerfile 21 | 22 | schedule: 23 | - cron: "0 0 * * 1" 24 | workflow_dispatch: 25 | 26 | jobs: 27 | build: 28 | runs-on: ubuntu-latest 29 | 30 | steps: 31 | - name: Checkout Repository 32 | uses: actions/checkout@v3 33 | 34 | - name: Set up Go 35 | uses: actions/setup-go@v4 36 | with: 37 | go-version: "1.23" 38 | 39 | - name: Install dependencies 40 | run: | 41 | sudo apt update 42 | sudo apt install -y \ 43 | ca-certificates jq curl 44 | 45 | - name: Install enumdns at specific commit 46 | run: | 47 | go install github.com/helviojunior/enumdns@${GITHUB_SHA} 48 | 49 | - name: Create a fake wordlist 50 | run: | 51 | cat << EOF > /tmp/wl.txt 52 | www 53 | wiki 54 | EOF 55 | 56 | - name: Verify Installation 57 | run: | 58 | enumdns version 59 | enumdns brute -d sec4us.com.br -w /tmp/wl.txt -o /tmp/result.txt 60 | 61 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | release: 3 | types: [created] 4 | 5 | jobs: 6 | releases-matrix: 7 | name: Release Go Binary 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | goos: [linux, windows, darwin] 12 | goarch: ["386", amd64, arm64] 13 | exclude: 14 | - goarch: "386" 15 | goos: darwin 16 | - goarch: arm64 17 | goos: windows 18 | - goarch: "386" 19 | goos: windows 20 | steps: 21 | - name: Checkout code 22 | uses: actions/checkout@v3 23 | with: 24 | fetch-depth: 0 25 | - name: Get OS and arch info 26 | id: vars 27 | run: | 28 | VER=$(echo '${{ github.event.release.tag_name }}' | grep -oE '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}') 29 | GITHASH=$(git rev-parse --short HEAD) 30 | BUILDENV=$(go version | cut -d' ' -f 3,4 | sed 's/ /_/g') 31 | BUILDTIME=$(date -u +'%Y-%m-%dT%H:%M:%SZ') 32 | LDFLAGS="-s -w \ 33 | -X=github.com/helviojunior/enumdns/internal/version.Version=$VER \ 34 | -X=github.com/helviojunior/enumdns/internal/version.GitHash=$GITHASH \ 35 | -X=github.com/helviojunior/enumdns/internal/version.GoBuildEnv=$BUILDENV \ 36 | -X=github.com/helviojunior/enumdns/internal/version.GoBuildTime=$BUILDTIME" 37 | 38 | echo "LDFLAGS=$LDFLAGS" >> $GITHUB_OUTPUT 39 | 40 | - uses: wangyoucao577/go-release-action@v1.33 41 | with: 42 | github_token: ${{ secrets.GITHUB_TOKEN }} 43 | goos: ${{ matrix.goos }} 44 | goarch: ${{ matrix.goarch }} 45 | goversion: "https://dl.google.com/go/go1.23.5.linux-amd64.tar.gz" 46 | ldflags: ${{ steps.vars.outputs.LDFLAGS }} 47 | 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | *.sqlite3 7 | *.jsonl 8 | *.txt 9 | enumdns 10 | enumdns.exe 11 | # dep vendor/ 12 | vendor/ 13 | 14 | # build artifacts 15 | build/ 16 | 17 | # screenshots dir 18 | screenshots/ 19 | 20 | # Test binary, build with `go test -c` 21 | *.test 22 | 23 | # Output of the go coverage tool, specifically when used with LiteIDE 24 | *.out 25 | 26 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 27 | .glide/ 28 | .DS_Store 29 | .idea/ -------------------------------------------------------------------------------- /INSTALL.md: -------------------------------------------------------------------------------- 1 | # EnumDNS instalation procedures 2 | 3 | ## Linux 4 | 5 | ``` 6 | apt install curl jq 7 | 8 | url=$(curl -s https://api.github.com/repos/helviojunior/enumdns/releases | jq -r '[ .[] | {id: .id, tag_name: .tag_name, assets: [ .assets[] | select(.name|match("linux-amd64.tar.gz$")) | {name: .name, browser_download_url: .browser_download_url} ]} | select(.assets != []) ] | sort_by(.id) | reverse | first(.[].assets[]) | .browser_download_url') 9 | 10 | cd /tmp 11 | rm -rf enumdns-latest.tar.gz enumdns 12 | wget -nv -O enumdns-latest.tar.gz "$url" 13 | tar -xzf enumdns-latest.tar.gz 14 | 15 | rsync -av enumdns /usr/local/sbin/ 16 | chmod +x /usr/local/sbin/enumdns 17 | 18 | enumdns version 19 | ``` 20 | 21 | ## MacOS 22 | 23 | ### Installing HomeBrew 24 | 25 | Note: Just run this command if you need to install HomeBrew to first time 26 | 27 | ``` 28 | /bin/bash -c "$(curl -fsSL raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" 29 | ``` 30 | 31 | ### Installing EnumDNS 32 | 33 | ``` 34 | brew install curl jq 35 | 36 | arch=$(if [[ "$(uname -m)" -eq "x86_64" ]]; then echo "amd64"; else echo "arm64"; fi) 37 | 38 | url=$(curl -s https://api.github.com/repos/helviojunior/enumdns/releases | jq -r --arg filename "darwin-${arch}.tar.gz\$" '[ .[] | {id: .id, tag_name: .tag_name, assets: [ .assets[] | select(.name|match($filename)) | {name: .name, browser_download_url: .browser_download_url} ]} | select(.assets != []) ] | sort_by(.id) | reverse | first(.[].assets[]) | .browser_download_url') 39 | 40 | cd /tmp 41 | rm -rf enumdns-latest.tar.gz enumdns 42 | curl -sS -L -o enumdns-latest.tar.gz "$url" 43 | tar -xzf enumdns-latest.tar.gz 44 | 45 | rsync -av enumdns /usr/local/sbin/ 46 | chmod +x /usr/local/sbin/enumdns 47 | 48 | enumdns version 49 | ``` 50 | 51 | ## Windows 52 | 53 | Just run the following powershell script 54 | 55 | ``` 56 | # Download latest helviojunior/enumdns release from github 57 | function Invoke-Downloadenumdns { 58 | 59 | $repo = "helviojunior/enumdns" 60 | 61 | # Determine OS and Architecture 62 | $osPlatform = [System.Runtime.InteropServices.RuntimeInformation]::OSDescription 63 | $architecture = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture 64 | 65 | if ($architecture -eq $null -or $architecture -eq "") { 66 | $architecture = $Env:PROCESSOR_ARCHITECTURE 67 | } 68 | 69 | if ($osPlatform -eq $null -or $osPlatform -eq "") { 70 | $osPlatform = $Env:OS 71 | } 72 | 73 | # Adjust the platform and architecture for the API call 74 | $platform = switch -Wildcard ($osPlatform) { 75 | "*Windows*" { "windows" } 76 | "*Linux*" { "linux" } 77 | "*Darwin*" { "darwin" } # MacOS is identified as Darwin 78 | Default { "unknown" } 79 | } 80 | $arch = switch ($architecture) { 81 | "X64" { "amd64" } 82 | "AMD64" { "amd64" } 83 | "X86" { "386" } 84 | "Arm" { "arm" } 85 | "Arm64" { "arm64" } 86 | Default { "unknown" } 87 | } 88 | 89 | if ($platform -eq "unknown" -or $arch -eq "unknown") { 90 | Write-Error "Cannot get OS Platform and Architecture" 91 | Return 92 | } 93 | 94 | Write-Host Getting release list 95 | $releases = "https://api.github.com/repos/$repo/releases" 96 | 97 | $asset = Invoke-WebRequest $releases | ConvertFrom-Json | Sort-Object -Descending -Property "Id" | ForEach-Object -Process { Get-AssetData -Release $_ -OSPlatform $platform -OSArchitecture $arch } | Select-Object -First 1 98 | 99 | if ($asset -eq $null -or $asset.browser_download_url -eq $null){ 100 | Write-Error "Cannot find a valid URL" 101 | Return 102 | } 103 | 104 | $tmpPath = $Env:Temp 105 | if ($tmpPath -eq $null -or $tmpPath -eq "") { 106 | $tmpPath = $Env:TMPDIR 107 | } 108 | if ($tmpPath -eq $null -or $tmpPath -eq "") { 109 | $tmpPath = switch ($platform) { 110 | "windows" { "c:\windows\temp\" } 111 | "linux" { "/tmp" } 112 | "darwin" { "/tmp" } 113 | } 114 | } 115 | 116 | $extension = switch ($platform) { 117 | "windows" { ".zip" } 118 | "linux" { ".tar.gz" } 119 | "darwin" { ".tar.gz" } # MacOS is identified as Darwin 120 | Default { "unknown" } 121 | } 122 | 123 | $file = "enumdns-latest$extension" 124 | 125 | Write-Host Dowloading latest release 126 | $zip = Join-Path -Path $tmpPath -ChildPath $file 127 | Remove-Item $zip -Force -ErrorAction SilentlyContinue 128 | Invoke-WebRequest $asset.browser_download_url -Out $zip 129 | 130 | Write-Host Extracting release files 131 | if ($extension -eq ".zip") { 132 | Expand-Archive $zip -Force -DestinationPath $tmpPath 133 | }else{ 134 | . tar -xzf "$zip" -C "$tmpPath" 135 | } 136 | 137 | $exeFilename = switch ($platform) { 138 | "windows" { "enumdns.exe" } 139 | "linux" { "enumdns" } 140 | "darwin" { "enumdns" } 141 | } 142 | 143 | try { 144 | $dstPath = (New-Object -ComObject Shell.Application).NameSpace('shell:Downloads').Self.Path 145 | } catch { 146 | $dstPath = switch ($platform) { 147 | "windows" { "~\Downloads\" } 148 | "linux" { "/usr/local/sbin/" } 149 | "darwin" { "/usr/local/sbin/" } 150 | } 151 | } 152 | 153 | try { 154 | $name = Join-Path -Path $dstPath -ChildPath $exeFilename 155 | 156 | # Cleaning up target dir 157 | Remove-Item $name -Recurse -Force -ErrorAction SilentlyContinue 158 | 159 | # Moving from temp dir to target dir 160 | Move-Item $(Join-Path -Path $tmpPath -ChildPath $exeFilename) -Destination $name -Force 161 | 162 | # Removing temp files 163 | Remove-Item $zip -Force 164 | } catch { 165 | $name = Join-Path -Path $tmpPath -ChildPath $exeFilename 166 | } 167 | 168 | Write-Host "PCAP Raptor saved at $name" -ForegroundColor DarkYellow 169 | 170 | Write-Host "Getting enumdns version banner" 171 | . $name version 172 | } 173 | 174 | Function Get-AssetData { 175 | [CmdletBinding(SupportsShouldProcess = $False)] 176 | [OutputType([object])] 177 | Param ( 178 | [Parameter(Mandatory = $True, Position = 0)] 179 | [object]$Release, 180 | [Parameter(Mandatory = $True, Position = 1)] 181 | [string]$OSPlatform, 182 | [Parameter(Mandatory = $True, Position = 2)] 183 | [string]$OSArchitecture 184 | ) 185 | 186 | if($Release -is [system.array]){ 187 | $Release = $Release[0] 188 | } 189 | 190 | if (Get-Member -inputobject $Release -name "assets" -Membertype Properties) { 191 | 192 | $extension = switch ($OSPlatform) { 193 | "windows" { ".zip" } 194 | "linux" { ".tar.gz" } 195 | "darwin" { ".tar.gz" } # MacOS is identified as Darwin 196 | Default { "unknown" } 197 | } 198 | 199 | foreach ($asset in $Release.assets) 200 | { 201 | If ($asset.name.Contains("enumdns-") -and $asset.name.Contains("$platform-$arch$extension")) { Return $asset } 202 | } 203 | 204 | } 205 | Return $null 206 | } 207 | 208 | Invoke-Downloadenumdns 209 | 210 | ``` 211 | 212 | 213 | # Build from source 214 | 215 | ## Linux environment 216 | 217 | Follows the suggest commands to install linux environment 218 | 219 | ### Installing Go v1.23.5 220 | 221 | ``` 222 | wget https://go.dev/dl/go1.23.5.linux-amd64.tar.gz 223 | rm -rf /usr/local/go && tar -C /usr/local -xzf go1.23.5.linux-amd64.tar.gz 224 | rm -rf /usr/bin/go && ln -s /usr/local/go/bin/go /usr/bin/go 225 | ``` 226 | 227 | ## Build enumdns 228 | 229 | Clone the repository and build the project with Golang: 230 | 231 | ``` 232 | git clone https://github.com/helviojunior/enumdns.git 233 | cd enumdns 234 | go get ./... 235 | go build 236 | ``` 237 | 238 | If you want to update go.sum file just run the command `go mod tidy`. 239 | 240 | ## Installing system wide 241 | 242 | After build run the commands bellow 243 | 244 | ``` 245 | go install . 246 | ln -s /root/go/bin/enumdns /usr/bin/enumdns 247 | ``` 248 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2022, Helvio Junior 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EnumDNS 2 | 3 | EnumDNS is a modular DNS reconnaissance tool capable of resolving hosts from various sources, including wordlists, BloodHound files, and Active Directory environments. 4 | 5 | Available modules: 6 | 7 | 1. Brute-force 8 | 2. Enumerate DNS registers (CNAME, A, AAAA, NS and so on) 9 | 3. Resolve DNS hosts from txt file 10 | 4. Resolve DNS hosts from BloodHound file (.zip or .json) 11 | 12 | 13 | ## Main features 14 | 15 | - [x] Perform brute-force DNS enumeration to discover hostnames 16 | - [x] Support for custom DNS suffix lists 17 | - [x] Automatically identify cloud provider services 18 | - [x] Retrieve multiple DNS record types (e.g., CNAME, A, AAAA) 19 | - [x] Enumerate all domain controllers names and IPs (in a Active Directory environment) 20 | - [x] Support to SOCKS (socks4/socks5) proxy 21 | - [x] Additional advanced features and enhancements 22 | 23 | 24 | ## Get last release 25 | 26 | Check how to get last release by your Operational Systems procedures here [INSTALL.md](https://github.com/helviojunior/enumdns/blob/main/INSTALL.md) 27 | 28 | 29 | # Utilization 30 | 31 | ``` 32 | $ enumdns -h 33 | 34 | 35 | ______ ____ _ _______ 36 | / ____/___ __ ______ ___ / __ \/ | / / ___/ 37 | / __/ / __ \/ / / / __ '__ \/ / / / |/ /\__ \ 38 | / /___/ / / / /_/ / / / / / / /_/ / /| /___/ / 39 | /_____/_/ /_/\__,_/_/ /_/ /_/_____/_/ |_//____/ 40 | 41 | Usage: 42 | enumdns [command] 43 | 44 | Examples: 45 | 46 | - enumdns recon -d helviojunior.com.br -o enumdns.txt 47 | - enumdns recon -d helviojunior.com.br --write-jsonl 48 | - enumdns recon -L domains.txt --write-db 49 | 50 | - enumdns brute -d helviojunior.com.br -w /tmp/wordlist.txt -o enumdns.txt 51 | - enumdns brute -d helviojunior.com.br -w /tmp/wordlist.txt --write-jsonl 52 | - enumdns brute -L domains.txt -w /tmp/wordlist.txt --write-db 53 | 54 | - enumdns resolve bloodhound -L /tmp/bloodhound_computers.json -o enumdns.txt 55 | - enumdns resolve bloodhound -L /tmp/bloodhound_files.zip --write-jsonl 56 | - enumdns resolve bloodhound -L /tmp/bloodhound_computers.json --write-db 57 | 58 | - enumdns resolve file -L /tmp/host_list.txt -o enumdns.txt 59 | - enumdns resolve file -L /tmp/host_list.txt --write-jsonl 60 | - enumdns resolve file -L /tmp/host_list.txt --write-db 61 | 62 | Available Commands: 63 | brute Perform brute-force enumeration 64 | help Help about any command 65 | recon Perform recon enumeration 66 | report Work with enumdns reports 67 | version Get the enumdns version 68 | 69 | Flags: 70 | -D, --debug-log Enable debug logging 71 | -h, --help help for enumdns 72 | -X, --proxy string Proxy to pass traffic through: (e.g., socks4://user:pass@proxy_host:1080 73 | -q, --quiet Silence (almost all) logging 74 | -o, --write-text-file string The file to write Text lines to 75 | 76 | Use "enumdns [command] --help" for more information about a command. 77 | 78 | ``` 79 | 80 | 81 | ## Disclaimer 82 | 83 | This tool is intended for educational purpose or for use in environments where you have been given explicit/legal authorization to do so. -------------------------------------------------------------------------------- /cmd/brute.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "log/slog" 6 | "os" 7 | "fmt" 8 | 9 | "github.com/helviojunior/enumdns/internal/ascii" 10 | "github.com/helviojunior/enumdns/internal/tools" 11 | "github.com/helviojunior/enumdns/pkg/log" 12 | "github.com/helviojunior/enumdns/pkg/runner" 13 | "github.com/helviojunior/enumdns/pkg/database" 14 | "github.com/helviojunior/enumdns/pkg/writers" 15 | "github.com/helviojunior/enumdns/pkg/readers" 16 | "github.com/spf13/cobra" 17 | ) 18 | 19 | var bruteRunner *runner.Runner 20 | 21 | var bruteWriters = []writers.Writer{} 22 | var bruteCmd = &cobra.Command{ 23 | Use: "brute", 24 | Short: "Perform brute-force enumeration", 25 | Long: ascii.LogoHelp(ascii.Markdown(` 26 | # brute 27 | 28 | Perform brute-force enumeration. 29 | 30 | By default, enumdns will only show information regarding the brute-force process. 31 | However, that is only half the fun! You can add multiple _writers_ that will 32 | collect information such as response codes, content, and more. You can specify 33 | multiple writers using the _--writer-*_ flags (see --help). 34 | `)), 35 | Example: ` 36 | - enumdns brute -d helviojunior.com.br -w /tmp/wordlist.txt -o enumdns.txt 37 | - enumdns brute -d helviojunior.com.br -w /tmp/wordlist.txt --write-jsonl 38 | - enumdns brute -L domains.txt -w /tmp/wordlist.txt --write-db`, 39 | PersistentPreRunE: func(cmd *cobra.Command, args []string) error { 40 | var err error 41 | 42 | // Annoying quirk, but because I'm overriding PersistentPreRun 43 | // here which overrides the parent it seems. 44 | // So we need to explicitly call the parent's one now. 45 | if err = rootCmd.PersistentPreRunE(cmd, args); err != nil { 46 | return err 47 | } 48 | 49 | // An slog-capable logger to use with drivers and runners 50 | logger := slog.New(log.Logger) 51 | 52 | // Configure writers that subcommand scanners will pass to 53 | // a runner instance. 54 | 55 | //The first one is the general writer (global user) 56 | w, err := writers.NewDbWriter(opts.Writer.CtrlDbURI, false) 57 | if err != nil { 58 | return err 59 | } 60 | bruteWriters = append(bruteWriters, w) 61 | 62 | //The second one is the STDOut 63 | if opts.Logging.Silence != true { 64 | w, err := writers.NewStdoutWriter() 65 | if err != nil { 66 | return err 67 | } 68 | bruteWriters = append(bruteWriters, w) 69 | } 70 | 71 | if opts.Writer.Text { 72 | w, err := writers.NewTextWriter(opts.Writer.TextFile) 73 | if err != nil { 74 | return err 75 | } 76 | bruteWriters = append(bruteWriters, w) 77 | } 78 | 79 | if opts.Writer.Jsonl { 80 | w, err := writers.NewJsonWriter(opts.Writer.JsonlFile) 81 | if err != nil { 82 | return err 83 | } 84 | bruteWriters = append(bruteWriters, w) 85 | } 86 | 87 | if opts.Writer.Db { 88 | w, err := writers.NewDbWriter(opts.Writer.DbURI, opts.Writer.DbDebug) 89 | if err != nil { 90 | return err 91 | } 92 | bruteWriters = append(bruteWriters, w) 93 | } 94 | 95 | if opts.Writer.Csv { 96 | w, err := writers.NewCsvWriter(opts.Writer.CsvFile) 97 | if err != nil { 98 | return err 99 | } 100 | bruteWriters = append(bruteWriters, w) 101 | } 102 | 103 | if opts.Writer.ELastic { 104 | w, err := writers.NewElasticWriter(opts.Writer.ELasticURI) 105 | if err != nil { 106 | return err 107 | } 108 | bruteWriters = append(bruteWriters, w) 109 | } 110 | 111 | if opts.Writer.None { 112 | w, err := writers.NewNoneWriter() 113 | if err != nil { 114 | return err 115 | } 116 | bruteWriters = append(bruteWriters, w) 117 | } 118 | 119 | if len(bruteWriters) == 0 { 120 | log.Warn("no writers have been configured. to persist probe results, add writers using --write-* flags") 121 | } 122 | 123 | // Get the runner up. Basically, all of the subcommands will use this. 124 | bruteRunner, err = runner.NewRunner(logger, *opts, bruteWriters) 125 | if err != nil { 126 | return err 127 | } 128 | 129 | fileOptions.DnsServer = opts.DnsServer + ":" + fmt.Sprintf("%d", opts.DnsPort) 130 | 131 | return nil 132 | }, 133 | PreRunE: func(cmd *cobra.Command, args []string) error { 134 | if opts.DnsSuffix == "" && fileOptions.DnsSuffixFile == "" { 135 | return errors.New("a DNS suffix or DNS suffix file must be specified") 136 | } 137 | 138 | if fileOptions.DnsSuffixFile != "" { 139 | if !tools.FileExists(fileOptions.DnsSuffixFile) { 140 | return errors.New("DNS suffix file is not readable") 141 | } 142 | } 143 | 144 | if fileOptions.HostFile == "" { 145 | return errors.New("a wordlist file must be specified") 146 | } 147 | 148 | if !tools.FileExists(fileOptions.HostFile) { 149 | return errors.New("wordlist file is not readable") 150 | } 151 | 152 | return nil 153 | }, 154 | Run: func(cmd *cobra.Command, args []string) { 155 | 156 | //Check DNS connectivity 157 | _, err := tools.GetValidDnsSuffix(fileOptions.DnsServer, "google.com.", opts.Proxy) 158 | if err != nil { 159 | log.Error("Error checking DNS connectivity", "err", err) 160 | os.Exit(2) 161 | } 162 | 163 | log.Debug("starting DNS brute-force") 164 | 165 | dnsSuffix := []string{} 166 | hostWordList := []string{} 167 | reader := readers.NewFileReader(fileOptions) 168 | total := 0 169 | 170 | if fileOptions.DnsSuffixFile != "" { 171 | log.Debugf("Reading dns suffix file: %s", fileOptions.DnsSuffixFile) 172 | if err := reader.ReadDnsList(&dnsSuffix); err != nil { 173 | log.Error("error in reader.Read", "err", err) 174 | log.Warn("If you are facing error related to 'SOA not found for domain' you can ignore it with -I option") 175 | os.Exit(2) 176 | } 177 | }else{ 178 | //Check if DNS exists 179 | s, err := tools.GetValidDnsSuffix(fileOptions.DnsServer, opts.DnsSuffix, opts.Proxy) 180 | if err != nil { 181 | log.Error("invalid dns suffix", "suffix", opts.DnsSuffix, "err", err) 182 | os.Exit(2) 183 | } 184 | dnsSuffix = append(dnsSuffix, s) 185 | } 186 | log.Debugf("Loaded %s DNS suffix(es)", tools.FormatInt(len(dnsSuffix))) 187 | 188 | log.Debugf("Reading dns word list file: %s", fileOptions.HostFile) 189 | if err := reader.ReadWordList(&hostWordList); err != nil { 190 | log.Error("error in reader.Read", "err", err) 191 | os.Exit(2) 192 | } 193 | total = len(dnsSuffix) * len(hostWordList) 194 | 195 | if len(dnsSuffix) == 0 { 196 | log.Error("DNS suffix list is empty") 197 | os.Exit(2) 198 | } 199 | 200 | log.Infof("Enumerating %s DNS hosts", tools.FormatInt(total)) 201 | 202 | // Check runned items 203 | conn, _ := database.Connection(opts.Writer.CtrlDbURI, true, false) 204 | 205 | go func() { 206 | defer close(bruteRunner.Targets) 207 | 208 | ascii.HideCursor() 209 | for _, s := range dnsSuffix { 210 | bruteRunner.Targets <- s 211 | for _, h := range hostWordList { 212 | 213 | i := true 214 | host := h + "." + s 215 | if !forceCheck { 216 | response := conn.Raw("SELECT count(id) as count from results WHERE failed = 0 AND fqdn = ?", host) 217 | if response != nil { 218 | var cnt int 219 | _ = response.Row().Scan(&cnt) 220 | i = (cnt == 0) 221 | if cnt > 0 { 222 | log.Debug("[Host already checked]", "fqdn", host) 223 | } 224 | } 225 | } 226 | 227 | if i || forceCheck{ 228 | bruteRunner.Targets <- host 229 | }else{ 230 | bruteRunner.AddSkiped() 231 | } 232 | } 233 | } 234 | 235 | }() 236 | 237 | bruteRunner.Run(total) 238 | bruteRunner.Close() 239 | 240 | }, 241 | } 242 | 243 | func init() { 244 | rootCmd.AddCommand(bruteCmd) 245 | 246 | bruteCmd.Flags().StringVarP(&opts.DnsSuffix, "dns-suffix", "d", "", "Single DNS suffix. (ex: helviojunior.com.br)") 247 | bruteCmd.Flags().StringVarP(&fileOptions.DnsSuffixFile, "dns-list", "L", "", "File containing a list of DNS suffix") 248 | bruteCmd.Flags().StringVarP(&fileOptions.HostFile, "word-list", "w", "", "File containing a list of DNS hosts") 249 | 250 | bruteCmd.Flags().BoolVarP(&fileOptions.IgnoreNonexistent, "IgnoreNonexistent", "I", false, "Ignore Nonexistent DNS suffix. Used only with --dns-list option.") 251 | 252 | bruteCmd.Flags().BoolVarP(&opts.Quick, "quick", "Q", false, "Request just A registers.") 253 | 254 | } -------------------------------------------------------------------------------- /cmd/recon.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "log/slog" 6 | "os" 7 | "fmt" 8 | 9 | "github.com/helviojunior/enumdns/internal/ascii" 10 | "github.com/helviojunior/enumdns/internal/tools" 11 | "github.com/helviojunior/enumdns/pkg/log" 12 | "github.com/helviojunior/enumdns/pkg/runner" 13 | //"github.com/helviojunior/enumdns/pkg/database" 14 | "github.com/helviojunior/enumdns/pkg/writers" 15 | "github.com/helviojunior/enumdns/pkg/readers" 16 | "github.com/spf13/cobra" 17 | ) 18 | 19 | var reconRunner *runner.Recon 20 | 21 | var reconWriters = []writers.Writer{} 22 | var reconCmd = &cobra.Command{ 23 | Use: "recon", 24 | Short: "Perform recon enumeration", 25 | Long: ascii.LogoHelp(ascii.Markdown(` 26 | # recon 27 | 28 | Perform recon enumeration. 29 | 30 | By default, enumdns will only show information regarding the recon process. 31 | However, that is only half the fun! You can add multiple _writers_ that will 32 | collect information such as response codes, content, and more. You can specify 33 | multiple writers using the _--writer-*_ flags (see --help). 34 | `)), 35 | Example: ` 36 | - enumdns recon -d helviojunior.com.br -o enumdns.txt 37 | - enumdns recon -d helviojunior.com.br --write-jsonl 38 | - enumdns recon -L domains.txt --write-db`, 39 | PersistentPreRunE: func(cmd *cobra.Command, args []string) error { 40 | var err error 41 | 42 | // Annoying quirk, but because I'm overriding PersistentPreRun 43 | // here which overrides the parent it seems. 44 | // So we need to explicitly call the parent's one now. 45 | if err = rootCmd.PersistentPreRunE(cmd, args); err != nil { 46 | return err 47 | } 48 | 49 | // An slog-capable logger to use with drivers and runners 50 | logger := slog.New(log.Logger) 51 | 52 | // Configure writers that subcommand scanners will pass to 53 | // a runner instance. 54 | 55 | //The first one is the general writer (global user) 56 | w, err := writers.NewDbWriter(opts.Writer.CtrlDbURI, false) 57 | if err != nil { 58 | return err 59 | } 60 | reconWriters = append(reconWriters, w) 61 | 62 | //The second one is the STDOut 63 | if opts.Logging.Silence != true { 64 | w, err := writers.NewStdoutWriter() 65 | if err != nil { 66 | return err 67 | } 68 | w.WriteAll = true 69 | reconWriters = append(reconWriters, w) 70 | } 71 | 72 | if opts.Writer.Text { 73 | w, err := writers.NewTextWriter(opts.Writer.TextFile) 74 | if err != nil { 75 | return err 76 | } 77 | reconWriters = append(reconWriters, w) 78 | } 79 | 80 | if opts.Writer.Jsonl { 81 | w, err := writers.NewJsonWriter(opts.Writer.JsonlFile) 82 | if err != nil { 83 | return err 84 | } 85 | reconWriters = append(reconWriters, w) 86 | } 87 | 88 | if opts.Writer.Db { 89 | w, err := writers.NewDbWriter(opts.Writer.DbURI, opts.Writer.DbDebug) 90 | if err != nil { 91 | return err 92 | } 93 | reconWriters = append(reconWriters, w) 94 | } 95 | 96 | if opts.Writer.Csv { 97 | w, err := writers.NewCsvWriter(opts.Writer.CsvFile) 98 | if err != nil { 99 | return err 100 | } 101 | reconWriters = append(reconWriters, w) 102 | } 103 | 104 | if opts.Writer.ELastic { 105 | w, err := writers.NewElasticWriter(opts.Writer.ELasticURI) 106 | if err != nil { 107 | return err 108 | } 109 | reconWriters = append(reconWriters, w) 110 | } 111 | 112 | if opts.Writer.None { 113 | w, err := writers.NewNoneWriter() 114 | if err != nil { 115 | return err 116 | } 117 | reconWriters = append(reconWriters, w) 118 | } 119 | 120 | if len(reconWriters) == 0 { 121 | log.Warn("no writers have been configured. to persist probe results, add writers using --write-* flags") 122 | } 123 | 124 | // Get the runner up. Basically, all of the subcommands will use this. 125 | reconRunner, err = runner.NewRecon(logger, *opts, reconWriters) 126 | if err != nil { 127 | return err 128 | } 129 | 130 | fileOptions.DnsServer = opts.DnsServer + ":" + fmt.Sprintf("%d", opts.DnsPort) 131 | 132 | return nil 133 | }, 134 | PreRunE: func(cmd *cobra.Command, args []string) error { 135 | if opts.DnsSuffix == "" && fileOptions.DnsSuffixFile == "" { 136 | return errors.New("a DNS suffix or DNS suffix file must be specified") 137 | } 138 | 139 | if fileOptions.DnsSuffixFile != "" { 140 | if !tools.FileExists(fileOptions.DnsSuffixFile) { 141 | return errors.New("DNS suffix file is not readable") 142 | } 143 | } 144 | 145 | return nil 146 | }, 147 | Run: func(cmd *cobra.Command, args []string) { 148 | 149 | //Check DNS connectivity 150 | _, err := tools.GetValidDnsSuffix(fileOptions.DnsServer, "google.com.", opts.Proxy) 151 | if err != nil { 152 | log.Error("Error checking DNS connectivity", "err", err) 153 | os.Exit(2) 154 | } 155 | 156 | log.Debug("starting DNS recon") 157 | 158 | dnsSuffix := []string{} 159 | enumeratedDomains := []string{} 160 | reader := readers.NewFileReader(fileOptions) 161 | total := 0 162 | 163 | if fileOptions.DnsSuffixFile != "" { 164 | log.Debugf("Reading dns suffix file: %s", fileOptions.DnsSuffixFile) 165 | if err := reader.ReadDnsList(&dnsSuffix); err != nil { 166 | log.Error("error in reader.Read", "err", err) 167 | log.Warn("If you are facing error related to 'SOA not found for domain' you can ignore it with -I option") 168 | os.Exit(2) 169 | } 170 | }else{ 171 | //Check if DNS exists 172 | s, err := tools.GetValidDnsSuffix(fileOptions.DnsServer, opts.DnsSuffix, opts.Proxy) 173 | if err != nil { 174 | log.Error("invalid dns suffix", "suffix", opts.DnsSuffix, "err", err) 175 | os.Exit(2) 176 | } 177 | dnsSuffix = append(dnsSuffix, s) 178 | } 179 | log.Debugf("Loaded %s DNS name(s)", tools.FormatInt(len(dnsSuffix))) 180 | 181 | total = len(dnsSuffix) 182 | 183 | if len(dnsSuffix) == 0 { 184 | log.Error("DNS suffix list is empty") 185 | os.Exit(2) 186 | } 187 | 188 | for len(dnsSuffix) > 0 { 189 | log.Warnf("Enumerating %s DNS hosts", tools.FormatInt(total)) 190 | 191 | go func() { 192 | defer close(reconRunner.Targets) 193 | 194 | ascii.HideCursor() 195 | 196 | for _, s := range dnsSuffix { 197 | reconRunner.Targets <- s 198 | enumeratedDomains = append(enumeratedDomains, s) 199 | } 200 | 201 | }() 202 | 203 | reconRunner.Run(total) 204 | 205 | dnsSuffix = []string{} 206 | 207 | for _, d := range reconRunner.Domains { 208 | if !tools.SliceHasStr(enumeratedDomains, d) && !tools.SliceHasStr(dnsSuffix, d) { 209 | dnsSuffix = append(dnsSuffix, d) 210 | } 211 | } 212 | 213 | total = len(dnsSuffix) 214 | if total > 0 { 215 | log.Infof("%s new domain(s) found", tools.FormatInt(total)) 216 | reconRunner.Reset() 217 | } 218 | 219 | } 220 | 221 | reconRunner.Close() 222 | 223 | }, 224 | } 225 | 226 | func init() { 227 | rootCmd.AddCommand(reconCmd) 228 | 229 | reconCmd.Flags().StringVarP(&opts.DnsSuffix, "dns-name", "d", "", "Single DNS suffix. (ex: helviojunior.com.br)") 230 | reconCmd.Flags().StringVarP(&fileOptions.DnsSuffixFile, "dns-list", "L", "", "File containing a list of DNS names") 231 | 232 | reconCmd.Flags().BoolVarP(&fileOptions.IgnoreNonexistent, "IgnoreNonexistent", "I", false, "Ignore Nonexistent DNS suffix. Used only with --dns-list option.") 233 | 234 | } -------------------------------------------------------------------------------- /cmd/report.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bufio" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "os" 9 | 10 | "github.com/helviojunior/enumdns/internal/ascii" 11 | "github.com/helviojunior/enumdns/pkg/database" 12 | "github.com/helviojunior/enumdns/pkg/log" 13 | "github.com/helviojunior/enumdns/pkg/models" 14 | "github.com/helviojunior/enumdns/pkg/writers" 15 | "github.com/spf13/cobra" 16 | "gorm.io/gorm/clause" 17 | ) 18 | 19 | var rptWriters = []writers.Writer{} 20 | var reportCmd = &cobra.Command{ 21 | Use: "report", 22 | Short: "Work with enumdns reports", 23 | Long: ascii.LogoHelp(ascii.Markdown(` 24 | # report 25 | 26 | Work with enumdns reports. 27 | `)), 28 | } 29 | 30 | func init() { 31 | rootCmd.AddCommand(reportCmd) 32 | } 33 | 34 | 35 | func convertFromDbTo(from string, writers []writers.Writer) error { 36 | log.Info("starting conversion...") 37 | 38 | var results = []*models.Result{} 39 | conn, err := database.Connection(fmt.Sprintf("sqlite:///%s", from), true, false) 40 | if err != nil { 41 | return err 42 | } 43 | 44 | if err := conn.Model(&models.Result{}).Preload(clause.Associations).Where("`exists` = ?", 1).Find(&results).Error; err != nil { 45 | return err 46 | } 47 | 48 | for _, result := range results { 49 | for _, w := range writers { 50 | if err := w.Write(result); err != nil { 51 | return err 52 | } 53 | } 54 | } 55 | 56 | log.Info("converted from a database", "rows", len(results)) 57 | return nil 58 | } 59 | 60 | 61 | func convertFromJsonlTo(from string, writers []writers.Writer) error { 62 | 63 | if len(writers) == 0 { 64 | log.Warn("no writers have been configured. to persist probe results, add writers using --write-* flags") 65 | } 66 | 67 | log.Info("starting conversion...") 68 | 69 | file, err := os.Open(from) 70 | if err != nil { 71 | return err 72 | } 73 | defer file.Close() 74 | 75 | var c = 0 76 | 77 | reader := bufio.NewReader(file) 78 | for { 79 | line, err := reader.ReadBytes('\n') 80 | if err != nil { 81 | if err == io.EOF { 82 | if len(line) == 0 { 83 | break // End of file 84 | } 85 | // Handle the last line without '\n' 86 | } else { 87 | return err 88 | } 89 | } 90 | 91 | var result models.Result 92 | if err := json.Unmarshal(line, &result); err != nil { 93 | log.Error("could not unmarshal JSON line", "err", err) 94 | continue 95 | } 96 | 97 | for _, w := range writers { 98 | if err := w.Write(&result); err != nil { 99 | return err 100 | } 101 | } 102 | 103 | c++ 104 | 105 | if err == io.EOF { 106 | break 107 | } 108 | } 109 | 110 | log.Info("converted from a JSON Lines file", "rows", c) 111 | return nil 112 | } -------------------------------------------------------------------------------- /cmd/report_convert.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | 5 | "errors" 6 | "fmt" 7 | "path/filepath" 8 | "strings" 9 | 10 | "github.com/helviojunior/enumdns/internal/ascii" 11 | "github.com/helviojunior/enumdns/internal/tools" 12 | "github.com/helviojunior/enumdns/pkg/log" 13 | "github.com/helviojunior/enumdns/pkg/writers" 14 | resolver "github.com/helviojunior/gopathresolver" 15 | "github.com/spf13/cobra" 16 | ) 17 | 18 | var conversionCmdExtensions = []string{".sqlite3", ".db", ".jsonl"} 19 | var convertCmdFlags = struct { 20 | fromFile string 21 | toFile string 22 | 23 | fromExt string 24 | toExt string 25 | }{} 26 | var convertCmd = &cobra.Command{ 27 | Use: "convert", 28 | Short: "Convert between SQLite and JSON Lines file formats", 29 | Long: ascii.LogoHelp(ascii.Markdown(` 30 | # report convert 31 | 32 | Convert between SQLite and JSON Lines file formats. 33 | 34 | A --from-file and --to-file must be specified. The extension used for the 35 | specified filenames will be used to determine the conversion direction and 36 | target.`)), 37 | Example: ascii.Markdown(` 38 | - enumdns report convert --from-file enumdns.sqlite3 --to-file enumdns.txt 39 | - enumdns report convert --from-file enumdns.sqlite3 --to-file data.jsonl 40 | - enumdns report convert --from-file enumdns.jsonl --to-file db.sqlite3`), 41 | PreRunE: func(cmd *cobra.Command, args []string) error { 42 | var err error 43 | 44 | if convertCmdFlags.fromFile == "" { 45 | return errors.New("from file not set") 46 | } 47 | if convertCmdFlags.toFile == "" { 48 | return errors.New("to file not set") 49 | } 50 | 51 | convertCmdFlags.fromFile, err = resolver.ResolveFullPath(convertCmdFlags.fromFile) 52 | if err != nil { 53 | return err 54 | } 55 | 56 | convertCmdFlags.toFile, err = resolver.ResolveFullPath(convertCmdFlags.toFile) 57 | if err != nil { 58 | return err 59 | } 60 | 61 | convertCmdFlags.fromExt = strings.ToLower(filepath.Ext(convertCmdFlags.fromFile)) 62 | convertCmdFlags.toExt = strings.ToLower(filepath.Ext(convertCmdFlags.toFile)) 63 | 64 | if convertCmdFlags.fromExt == "" || convertCmdFlags.toExt == "" { 65 | return errors.New("source and destination files must have extensions") 66 | } 67 | 68 | if convertCmdFlags.fromExt == convertCmdFlags.toExt { 69 | return errors.New("👀 source and destination file types must be different") 70 | } 71 | 72 | if convertCmdFlags.fromFile == convertCmdFlags.toFile { 73 | return errors.New("source and destination files cannot be the same") 74 | } 75 | 76 | if !tools.SliceHasStr(conversionCmdExtensions, convertCmdFlags.fromExt) { 77 | return errors.New("unsupported from file type") 78 | } 79 | if !tools.SliceHasStr(conversionCmdExtensions, convertCmdFlags.toExt) && convertCmdFlags.toExt != ".txt" { 80 | return errors.New("unsupported to file type") 81 | } 82 | 83 | return nil 84 | }, 85 | Run: func(cmd *cobra.Command, args []string) { 86 | var writer writers.Writer 87 | var err error 88 | if convertCmdFlags.toExt == ".sqlite3" || convertCmdFlags.toExt == ".db" { 89 | writer, err = writers.NewDbWriter(fmt.Sprintf("sqlite:///%s", convertCmdFlags.toFile), false) 90 | if err != nil { 91 | log.Error("could not get a database writer up", "err", err) 92 | return 93 | } 94 | } else if convertCmdFlags.toExt == ".jsonl" { 95 | toFile, err := tools.CreateFileWithDir(convertCmdFlags.toFile) 96 | if err != nil { 97 | log.Error("could not create target file", "err", err) 98 | return 99 | } 100 | writer, err = writers.NewJsonWriter(toFile) 101 | if err != nil { 102 | log.Error("could not get a JSON writer up", "err", err) 103 | return 104 | } 105 | } else if convertCmdFlags.toExt == ".txt" { 106 | toFile, err := tools.CreateFileWithDir(convertCmdFlags.toFile) 107 | if err != nil { 108 | log.Error("could not create target file", "err", err) 109 | return 110 | } 111 | writer, err = writers.NewTextWriter(toFile) 112 | if err != nil { 113 | log.Error("could not get a JSON writer up", "err", err) 114 | return 115 | } 116 | 117 | } 118 | 119 | rptWriters = append(rptWriters, writer) 120 | 121 | if convertCmdFlags.fromExt == ".sqlite3" || convertCmdFlags.fromExt == ".db" { 122 | if err := convertFromDbTo(convertCmdFlags.fromFile, rptWriters); err != nil { 123 | log.Error("failed to convert from SQLite", "err", err) 124 | return 125 | } 126 | } else if convertCmdFlags.fromExt == ".jsonl" { 127 | if err := convertFromJsonlTo(convertCmdFlags.fromFile, rptWriters); err != nil { 128 | log.Error("failed to convert from JSON Lines", "err", err) 129 | return 130 | } 131 | } 132 | 133 | for _, w := range rptWriters { 134 | w.Finish() 135 | } 136 | }, 137 | } 138 | 139 | func init() { 140 | reportCmd.AddCommand(convertCmd) 141 | 142 | convertCmd.Flags().StringVar(&convertCmdFlags.fromFile, "from-file", "", "The file to convert from") 143 | convertCmd.Flags().StringVar(&convertCmdFlags.toFile, "to-file", "", "The file to convert to. Use .sqlite3 for conversion to SQLite, and .jsonl for conversion to JSON Lines") 144 | } 145 | -------------------------------------------------------------------------------- /cmd/report_elastic.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "path/filepath" 6 | "strings" 7 | 8 | "github.com/helviojunior/enumdns/internal/ascii" 9 | "github.com/helviojunior/enumdns/internal/tools" 10 | "github.com/helviojunior/enumdns/pkg/log" 11 | "github.com/helviojunior/enumdns/pkg/writers" 12 | resolver "github.com/helviojunior/gopathresolver" 13 | "github.com/spf13/cobra" 14 | 15 | ) 16 | 17 | var elkCmdExtensions = []string{".sqlite3", ".db", ".jsonl"} 18 | var elkCmdFlags = struct { 19 | fromFile string 20 | fromExt string 21 | elasticURI string 22 | }{} 23 | var elkCmd = &cobra.Command{ 24 | Use: "elastic", 25 | Short: "Sync from local SQLite or JSON Lines file formats to Elastic", 26 | Long: ascii.LogoHelp(ascii.Markdown(` 27 | # report elastic 28 | 29 | Sync from local SQLite or JSON Lines file formats to Elastic. 30 | 31 | A --from-file and --elasticsearch-uri must be specified.`)), 32 | Example: ascii.Markdown(` 33 | - enumdns report elastic --from-file enumdns.sqlite3 --elasticsearch-uri http://localhost:9200/enumdns 34 | - enumdns report elastic --from-file enumdns.jsonl --elasticsearch-uri http://localhost:9200/enumdns`), 35 | PreRunE: func(cmd *cobra.Command, args []string) error { 36 | var err error 37 | 38 | if elkCmdFlags.fromFile == "" { 39 | return errors.New("from file not set") 40 | } 41 | 42 | elkCmdFlags.fromFile, err = resolver.ResolveFullPath(elkCmdFlags.fromFile) 43 | if err != nil { 44 | return err 45 | } 46 | 47 | elkCmdFlags.fromExt = strings.ToLower(filepath.Ext(elkCmdFlags.fromFile)) 48 | 49 | if elkCmdFlags.fromExt == "" { 50 | return errors.New("source file must have extension") 51 | } 52 | 53 | if !tools.SliceHasStr(elkCmdExtensions, elkCmdFlags.fromExt) { 54 | return errors.New("unsupported from file type") 55 | } 56 | 57 | return nil 58 | }, 59 | Run: func(cmd *cobra.Command, args []string) { 60 | var writer writers.Writer 61 | var err error 62 | 63 | log.Info("Checking Elasticsearch indexes...") 64 | writer, err = writers.NewElasticWriter(elkCmdFlags.elasticURI) 65 | if err != nil { 66 | log.Error("could not get a elastic writer up", "err", err) 67 | return 68 | } 69 | 70 | rptWriters = append(rptWriters, writer) 71 | 72 | if elkCmdFlags.fromExt == ".sqlite3" || elkCmdFlags.fromExt == ".db" { 73 | if err := convertFromDbTo(elkCmdFlags.fromFile, rptWriters); err != nil { 74 | log.Error("failed to convert from SQLite", "err", err) 75 | return 76 | } 77 | } else if elkCmdFlags.fromExt == ".jsonl" { 78 | if err := convertFromJsonlTo(elkCmdFlags.fromFile, rptWriters); err != nil { 79 | log.Error("failed to convert from JSON Lines", "err", err) 80 | return 81 | } 82 | } 83 | }, 84 | } 85 | 86 | func init() { 87 | reportCmd.AddCommand(elkCmd) 88 | 89 | elkCmd.Flags().StringVar(&elkCmdFlags.fromFile, "from-file", "~/.enumdns.db", "The file to convert from. Use .sqlite3 for conversion from SQLite, and .jsonl for conversion from JSON Lines") 90 | elkCmd.Flags().StringVar(&elkCmdFlags.elasticURI, "elasticsearch-uri", "http://localhost:9200/enumdns", "The elastic search URI to use. (e.g., http://user:pass@host:9200/index)") 91 | 92 | } 93 | -------------------------------------------------------------------------------- /cmd/resolve.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "log/slog" 6 | //"os" 7 | //"fmt" 8 | 9 | "github.com/helviojunior/enumdns/internal/ascii" 10 | "github.com/helviojunior/enumdns/internal/tools" 11 | "github.com/helviojunior/enumdns/pkg/log" 12 | "github.com/helviojunior/enumdns/pkg/runner" 13 | //"github.com/helviojunior/enumdns/pkg/database" 14 | "github.com/helviojunior/enumdns/pkg/writers" 15 | //"github.com/helviojunior/enumdns/pkg/readers" 16 | "github.com/spf13/cobra" 17 | ) 18 | 19 | var resolveRunner *runner.Runner 20 | 21 | var resolveWriters = []writers.Writer{} 22 | var resolveCmd = &cobra.Command{ 23 | Use: "resolve", 24 | Short: "Perform resolve roperations", 25 | Long: ascii.LogoHelp(ascii.Markdown(` 26 | # resolve 27 | 28 | Perform resolver operations. 29 | `)), 30 | Example: ` 31 | - enumdns resolve bloodhound -L /tmp/bloodhound_computers.json -o enumdns.txt 32 | - enumdns resolve bloodhound -L /tmp/bloodhound_files.zip --write-jsonl 33 | - enumdns resolve bloodhound -L /tmp/bloodhound_computers.json --write-db 34 | 35 | - enumdns resolve file -L /tmp/host_list.txt -o enumdns.txt 36 | - enumdns resolve file -L /tmp/host_list.txt --write-jsonl 37 | - enumdns resolve file -L /tmp/host_list.txt --write-db`, 38 | PersistentPreRunE: func(cmd *cobra.Command, args []string) error { 39 | var err error 40 | 41 | // Annoying quirk, but because I'm overriding PersistentPreRun 42 | // here which overrides the parent it seems. 43 | // So we need to explicitly call the parent's one now. 44 | if err = rootCmd.PersistentPreRunE(cmd, args); err != nil { 45 | return err 46 | } 47 | 48 | // An slog-capable logger to use with drivers and runners 49 | logger := slog.New(log.Logger) 50 | 51 | // Configure writers that subcommand scanners will pass to 52 | // a runner instance. 53 | 54 | //The first one is the general writer (global user) 55 | w, err := writers.NewDbWriter(opts.Writer.CtrlDbURI, opts.Writer.DbDebug) 56 | if err != nil { 57 | return err 58 | } 59 | resolveWriters = append(resolveWriters, w) 60 | 61 | //The second one is the STDOut 62 | if opts.Logging.Silence != true { 63 | w, err := writers.NewStdoutWriter() 64 | if err != nil { 65 | return err 66 | } 67 | resolveWriters = append(resolveWriters, w) 68 | } 69 | 70 | if opts.Writer.Text { 71 | w, err := writers.NewTextWriter(opts.Writer.TextFile) 72 | if err != nil { 73 | return err 74 | } 75 | resolveWriters = append(resolveWriters, w) 76 | } 77 | 78 | if opts.Writer.Jsonl { 79 | w, err := writers.NewJsonWriter(opts.Writer.JsonlFile) 80 | if err != nil { 81 | return err 82 | } 83 | resolveWriters = append(resolveWriters, w) 84 | } 85 | 86 | if opts.Writer.Db { 87 | w, err := writers.NewDbWriter(opts.Writer.DbURI, opts.Writer.DbDebug) 88 | if err != nil { 89 | return err 90 | } 91 | resolveWriters = append(resolveWriters, w) 92 | } 93 | 94 | if opts.Writer.Csv { 95 | w, err := writers.NewCsvWriter(opts.Writer.CsvFile) 96 | if err != nil { 97 | return err 98 | } 99 | resolveWriters = append(resolveWriters, w) 100 | } 101 | 102 | if opts.Writer.ELastic { 103 | w, err := writers.NewElasticWriter(opts.Writer.ELasticURI) 104 | if err != nil { 105 | return err 106 | } 107 | resolveWriters = append(resolveWriters, w) 108 | } 109 | 110 | if opts.Writer.None { 111 | w, err := writers.NewNoneWriter() 112 | if err != nil { 113 | return err 114 | } 115 | resolveWriters = append(resolveWriters, w) 116 | } 117 | 118 | if len(resolveWriters) == 0 { 119 | log.Warn("no writers have been configured. to persist probe results, add writers using --write-* flags") 120 | } 121 | 122 | // Get the runner up. Basically, all of the subcommands will use this. 123 | resolveRunner, err = runner.NewRunner(logger, *opts, resolveWriters) 124 | if err != nil { 125 | return err 126 | } 127 | 128 | return nil 129 | }, 130 | PreRunE: func(cmd *cobra.Command, args []string) error { 131 | //Check DNS connectivity 132 | _, err := tools.GetValidDnsSuffix(fileOptions.DnsServer, "google.com.", opts.Proxy) 133 | if err != nil { 134 | return errors.New("Error checking DNS connectivity: " + err.Error()) 135 | } 136 | 137 | return nil 138 | }, 139 | } 140 | 141 | func init() { 142 | rootCmd.AddCommand(resolveCmd) 143 | 144 | resolveCmd.PersistentFlags().BoolVarP(&fileOptions.IgnoreNonexistent, "IgnoreNonexistent", "I", false, "Ignore Nonexistent DNS suffix.") 145 | 146 | } -------------------------------------------------------------------------------- /cmd/resolve_bloodhound.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | //"log/slog" 6 | "io" 7 | "os" 8 | "strings" 9 | "path/filepath" 10 | "bufio" 11 | "encoding/json" 12 | 13 | "github.com/helviojunior/enumdns/internal/ascii" 14 | "github.com/helviojunior/enumdns/internal/tools" 15 | "github.com/helviojunior/enumdns/pkg/log" 16 | "github.com/helviojunior/enumdns/pkg/runner" 17 | "github.com/helviojunior/enumdns/pkg/database" 18 | "github.com/helviojunior/enumdns/pkg/writers" 19 | //"github.com/helviojunior/enumdns/pkg/readers" 20 | resolver "github.com/helviojunior/gopathresolver" 21 | "github.com/spf13/cobra" 22 | ) 23 | 24 | var zipTempFolder = "" 25 | var resolveBloodhoundExtensions = []string{".zip", ".json"} 26 | var resolveBloodhoundWriters = []writers.Writer{} 27 | var resolveBloodhoundCmd = &cobra.Command{ 28 | Use: "bloodhound", 29 | Short: "Perform resolve roperations", 30 | Long: ascii.LogoHelp(ascii.Markdown(` 31 | # resolve bloodhound 32 | 33 | Perform resolver operations. 34 | `)), 35 | Example: ` 36 | - enumdns resolve bloodhound -L /tmp/bloodhound_computers.json -o enumdns.txt 37 | - enumdns resolve bloodhound -L /tmp/bloodhound_files.zip --write-jsonl 38 | - enumdns resolve bloodhound -L /tmp/bloodhound_computers.json --write-db`, 39 | PersistentPreRunE: func(cmd *cobra.Command, args []string) error { 40 | // Annoying quirk, but because I'm overriding PersistentPreRun 41 | // here which overrides the parent it seems. 42 | // So we need to explicitly call the parent's one now. 43 | if err := resolveCmd.PersistentPreRunE(cmd, args); err != nil { 44 | return err 45 | } 46 | 47 | return nil 48 | }, 49 | PreRunE: func(cmd *cobra.Command, args []string) error { 50 | var err error 51 | if fileOptions.HostFile == "" { 52 | return errors.New("a Bloodhound file must be specified") 53 | } 54 | 55 | if !tools.FileExists(fileOptions.HostFile) { 56 | return errors.New("Bloodhound file is not readable") 57 | } 58 | 59 | fileOptions.HostFile, err = resolver.ResolveFullPath(fileOptions.HostFile) 60 | if err != nil { 61 | return err 62 | } 63 | 64 | fromExt := strings.ToLower(filepath.Ext(fileOptions.HostFile)) 65 | 66 | if fromExt == "" { 67 | return errors.New("Bloodhound file must have extension") 68 | } 69 | 70 | if !tools.SliceHasStr(resolveBloodhoundExtensions, fromExt) { 71 | return errors.New("Unsupported Bloodhound file type") 72 | } 73 | 74 | if err = resolveCmd.PreRunE(cmd, args); err != nil { 75 | return err 76 | } 77 | 78 | return nil 79 | }, 80 | Run: func(cmd *cobra.Command, args []string) { 81 | var err error 82 | log.Debug("starting DNS resolver with Bloodhound computers") 83 | 84 | hostWordList := []string{} 85 | domainList := []string{} 86 | total := 0 87 | 88 | log.Debugf("Reading Bloodhound file: %s", fileOptions.HostFile) 89 | fromExt := strings.ToLower(filepath.Ext(fileOptions.HostFile)) 90 | if fromExt == ".zip" { 91 | fileOptions.HostFile, err = getComputersFile(fileOptions.HostFile) 92 | if err != nil { 93 | log.Error("error extracting zip file", "err", err) 94 | os.Exit(2) 95 | } 96 | } 97 | 98 | err = readComputerFile(fileOptions.HostFile, &hostWordList, &domainList) 99 | 100 | if zipTempFolder != "" { 101 | tools.RemoveFolder(zipTempFolder) 102 | } 103 | if err != nil { 104 | log.Error("error reading json file", "err", err) 105 | os.Exit(2) 106 | } 107 | 108 | total = len(hostWordList) 109 | 110 | if len(hostWordList) == 0 { 111 | log.Error("DNS host list is empty") 112 | os.Exit(2) 113 | } 114 | 115 | log.Infof("Checking connection with %s domain(s)", tools.FormatInt(len(domainList))) 116 | for _, d := range domainList { 117 | _, err := tools.GetValidDnsSuffix(fileOptions.DnsServer, strings.Trim(d, ". ") + ".", opts.Proxy) 118 | if err != nil { 119 | log.Error("Error checking DNS connectivity. Try to ise -s option to set the DC ip", "err", err) 120 | if !fileOptions.IgnoreNonexistent { 121 | os.Exit(2) 122 | } 123 | }else{ 124 | log.Infof("%s: OK", strings.Trim(d, ". ")) 125 | } 126 | } 127 | 128 | log.Warnf("Enumerating %s Domains", tools.FormatInt(len(domainList))) 129 | 130 | reconRunner, err := runner.NewRecon(resolveRunner.GetLog(), *opts, resolveWriters) 131 | if err == nil { 132 | 133 | go func() { 134 | defer close(reconRunner.Targets) 135 | 136 | ascii.HideCursor() 137 | 138 | for _, d := range domainList { 139 | reconRunner.Targets <- d 140 | } 141 | 142 | }() 143 | 144 | reconRunner.Run(total) 145 | } 146 | 147 | log.Warnf("Enumerating %s DNS hosts", tools.FormatInt(total)) 148 | 149 | // Check runned items 150 | conn, _ := database.Connection(opts.Writer.CtrlDbURI, true, false) 151 | 152 | go func() { 153 | defer close(resolveRunner.Targets) 154 | 155 | ascii.HideCursor() 156 | 157 | for _, h := range hostWordList { 158 | 159 | i := true 160 | host := strings.Trim(h, ". ") + "." 161 | if !forceCheck { 162 | response := conn.Raw("SELECT count(id) as count from results WHERE failed = 0 AND fqdn = ?", host) 163 | if response != nil { 164 | var cnt int 165 | _ = response.Row().Scan(&cnt) 166 | i = (cnt == 0) 167 | if cnt > 0 { 168 | log.Debug("[Host already checked]", "fqdn", host) 169 | } 170 | } 171 | } 172 | 173 | if i || forceCheck{ 174 | resolveRunner.Targets <- host 175 | }else{ 176 | resolveRunner.AddSkiped() 177 | } 178 | } 179 | 180 | 181 | }() 182 | 183 | st := resolveRunner.Run(total) 184 | resolveRunner.Close() 185 | 186 | if st.Skiped > 0 { 187 | log.Warnf("%d hosts were skipped because they were already scanned. Use the --force parameter to rescan them.", st.Skiped) 188 | ascii.ClearLine() 189 | } 190 | 191 | }, 192 | } 193 | 194 | func init() { 195 | resolveCmd.AddCommand(resolveBloodhoundCmd) 196 | 197 | resolveBloodhoundCmd.Flags().StringVarP(&fileOptions.HostFile, "bloodhound-file", "L", "", "Bloodhound outoput file (.zip or _computers.json") 198 | } 199 | 200 | func getComputersFile(file_path string) (string, error) { 201 | var mime string 202 | var dst string 203 | var err error 204 | file_name := filepath.Base(file_path) 205 | logger := log.With("file", file_name) 206 | 207 | logger.Debug("Checking file") 208 | if mime, err = tools.GetMimeType(file_path); err != nil { 209 | logger.Debug("Error getting mime type", "err", err) 210 | return "", err 211 | } 212 | 213 | logger.Debug("Mime type", "mime", mime) 214 | if mime != "application/zip" { 215 | return "", errors.New("invalid file type") 216 | } 217 | 218 | if zipTempFolder, err = tools.CreateDir(tools.TempFileName("", "intelparser_", "")); err != nil { 219 | logger.Debug("Error creating temp folder to extract zip file", "err", err) 220 | return "", err 221 | } 222 | 223 | if dst, err = tools.CreateDirFromFilename(zipTempFolder, file_path); err != nil { 224 | logger.Debug("Error creating temp folder to extract zip file", "err", err) 225 | return "", err 226 | } 227 | 228 | if err = tools.Unzip(file_path, dst); err != nil { 229 | logger.Debug("Error extracting zip file", "temp_folder", dst, "err", err) 230 | return "", err 231 | } 232 | 233 | entries, err := os.ReadDir(dst) 234 | if err != nil { 235 | logger.Debug("Error listing folder files", "temp_folder", dst, "err", err) 236 | return "", err 237 | } 238 | 239 | for _, e := range entries { 240 | logger.Debug(e.Name()) 241 | if strings.Contains(strings.ToLower(e.Name()), "_computers.json"){ 242 | return filepath.Join(dst, e.Name()), nil 243 | } 244 | } 245 | 246 | return "", errors.New("computer file not found") 247 | 248 | } 249 | 250 | func readComputerFile(fileName string, outList *[]string, domainList *[]string) error { 251 | 252 | f, err := os.Open(fileName) 253 | if err != nil { 254 | return err 255 | } 256 | defer f.Close() 257 | 258 | br := bufio.NewReader(f) 259 | r, _, err := br.ReadRune() 260 | if err != nil { 261 | return err 262 | } 263 | if r != '\uFEFF' { 264 | br.UnreadRune() // Not a BOM -- put the rune back 265 | } 266 | 267 | fileBytes, err := io.ReadAll(br) 268 | if err != nil { 269 | return err 270 | } 271 | 272 | data := &computerFileData{} 273 | err = json.Unmarshal(fileBytes, data) 274 | if err != nil { 275 | return err 276 | } 277 | 278 | for _, c := range data.Data { 279 | n := strings.ToLower(c.Properties.Name) 280 | if c.Properties.Enabled { 281 | d := strings.ToLower(c.Properties.Domain) 282 | if !tools.SliceHasStr(*domainList, d) { 283 | *domainList = append(*domainList, d) 284 | } 285 | 286 | *outList = append(*outList, n) 287 | }else{ 288 | log.Debug("Computer disabled, ignoring.", "Name", n) 289 | } 290 | } 291 | 292 | return nil 293 | } 294 | 295 | type computerDataProperties struct { 296 | Name string `json:"name"` 297 | Domain string `json:"domain"` 298 | Enabled bool `json:"enabled"` 299 | } 300 | 301 | type computerData struct { 302 | ObjectIdentifier string `json:"ObjectIdentifier"` 303 | Properties computerDataProperties `json:"Properties"` 304 | } 305 | 306 | type computerFileData struct { 307 | 308 | Data []computerData `json:"data"` 309 | 310 | } -------------------------------------------------------------------------------- /cmd/resolve_crtsh.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "log/slog" 6 | "os" 7 | "time" 8 | 9 | "github.com/helviojunior/enumdns/internal/ascii" 10 | "github.com/helviojunior/enumdns/internal/tools" 11 | "github.com/helviojunior/enumdns/pkg/log" 12 | "github.com/helviojunior/enumdns/pkg/runner" 13 | "github.com/helviojunior/enumdns/pkg/database" 14 | "github.com/helviojunior/enumdns/pkg/writers" 15 | "github.com/helviojunior/enumdns/pkg/readers" 16 | "github.com/helviojunior/enumdns/pkg/models" 17 | resolver "github.com/helviojunior/gopathresolver" 18 | "github.com/spf13/cobra" 19 | ) 20 | 21 | var fqdnOutFile = "" 22 | var resolveCrtshWriters = []writers.Writer{} 23 | var resolveCrtshCmd = &cobra.Command{ 24 | Use: "crtsh", 25 | Short: "Perform resolve roperations", 26 | Long: ascii.LogoHelp(ascii.Markdown(` 27 | # resolve crtsh 28 | 29 | Perform cert.sh crawler + resolve enumeration. 30 | 31 | By default, enumdns will only show information regarding the resolver process. 32 | However, that is only half the fun! You can add multiple _writers_ that will 33 | collect information such as response codes, content, and more. You can specify 34 | multiple writers using the _--writer-*_ flags (see --help). 35 | `)), 36 | Example: ` 37 | - enumdns resolve crtsh -d helviojunior.com.br -o enumdns.txt 38 | - enumdns resolve crtsh -L domains.txt --write-jsonl 39 | - enumdns resolve crtsh -L domains.txt --write-db`, 40 | PersistentPreRunE: func(cmd *cobra.Command, args []string) error { 41 | var err error 42 | // Annoying quirk, but because I'm overriding PersistentPreRun 43 | // here which overrides the parent it seems. 44 | // So we need to explicitly call the parent's one now. 45 | if err := resolveCmd.PersistentPreRunE(cmd, args); err != nil { 46 | return err 47 | } 48 | 49 | // An slog-capable logger to use with drivers and runners 50 | logger := slog.New(log.Logger) 51 | 52 | 53 | if len(resolveWriters) == 0 { 54 | log.Warn("no writers have been configured. to persist probe results, add writers using --write-* flags") 55 | } 56 | 57 | // Get the runner up. Basically, all of the subcommands will use this. 58 | bruteRunner, err = runner.NewRunner(logger, *opts, resolveWriters) 59 | if err != nil { 60 | return err 61 | } 62 | 63 | 64 | return nil 65 | }, 66 | PreRunE: func(cmd *cobra.Command, args []string) error { 67 | var err error 68 | 69 | if opts.DnsSuffix == "" && fileOptions.DnsSuffixFile == "" { 70 | return errors.New("a DNS suffix or DNS suffix file must be specified") 71 | } 72 | 73 | if fileOptions.DnsSuffixFile != "" { 74 | if !tools.FileExists(fileOptions.DnsSuffixFile) { 75 | return errors.New("DNS suffix file is not readable") 76 | } 77 | } 78 | 79 | if fqdnOutFile != "" { 80 | fqdnOutFile, err = resolver.ResolveFullPath(fqdnOutFile) 81 | if err != nil { 82 | return err 83 | } 84 | } 85 | 86 | if err := resolveCmd.PreRunE(cmd, args); err != nil { 87 | return err 88 | } 89 | 90 | return nil 91 | 92 | }, 93 | Run: func(cmd *cobra.Command, args []string) { 94 | 95 | crtshOpts := &readers.CrtShReaderOptions{ 96 | Timeout : 300 * time.Second, 97 | ProxyUri : opts.Proxy, 98 | } 99 | 100 | dnsSuffix := []string{} 101 | hostWordList := []string{} 102 | fqdnList := []string{} 103 | reader := readers.NewFileReader(fileOptions) 104 | crtShReader := readers.NewCrtShReader(crtshOpts) 105 | total := 0 106 | 107 | if fileOptions.DnsSuffixFile != "" { 108 | log.Debugf("Reading dns suffix file: %s", fileOptions.DnsSuffixFile) 109 | if err := reader.ReadDnsList(&dnsSuffix); err != nil { 110 | log.Error("error in reader.Read", "err", err) 111 | log.Warn("If you are facing error related to 'SOA not found for domain' you can ignore it with -I option") 112 | os.Exit(2) 113 | } 114 | }else{ 115 | //Check if DNS exists 116 | s, err := tools.GetValidDnsSuffix(fileOptions.DnsServer, opts.DnsSuffix, opts.Proxy) 117 | if err != nil { 118 | log.Error("invalid dns suffix", "suffix", opts.DnsSuffix, "err", err) 119 | os.Exit(2) 120 | } 121 | dnsSuffix = append(dnsSuffix, s) 122 | } 123 | log.Debugf("Loaded %s DNS suffix(es)", tools.FormatInt(len(dnsSuffix))) 124 | 125 | if len(dnsSuffix) == 0 { 126 | log.Error("DNS suffix list is empty") 127 | os.Exit(2) 128 | } 129 | 130 | log.Debug("starting https://crt.sh crawler") 131 | for _, d := range dnsSuffix { 132 | log.Debugf("Reading dns prefix from Crt.sh to %s", d) 133 | if err := crtShReader.ReadFromCrtsh(d, &hostWordList, &fqdnList); err != nil { 134 | log.Error("error getting data from Crt.sh", "err", err) 135 | os.Exit(2) 136 | } 137 | } 138 | 139 | if len(hostWordList) == 0 { 140 | log.Error("DNS host list is empty") 141 | os.Exit(2) 142 | } 143 | 144 | total = len(dnsSuffix) * len(hostWordList) 145 | 146 | 147 | t := time.Now() 148 | for _, s := range fqdnList { 149 | for _, w := range resolveWriters { 150 | fqdn := &models.FQDNData{ 151 | FQDN : s, 152 | Source : "crt.sh", 153 | ProbedAt : t, 154 | } 155 | w.WriteFqdn(fqdn) 156 | } 157 | } 158 | 159 | if fqdnOutFile != "" { 160 | file, err := os.OpenFile(fqdnOutFile, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644) 161 | if err != nil { 162 | log.Error("Error writting FQDN file", "err", err) 163 | os.Exit(2) 164 | } 165 | defer file.Close() 166 | 167 | for _, s := range fqdnList { 168 | if _, err := file.WriteString(s + "\r\n"); err != nil { 169 | log.Error("Error writting FQDN file file", "line", s, "err", err) 170 | os.Exit(2) 171 | } 172 | } 173 | 174 | log.Infof("FQDN list file saved at %s", fqdnOutFile) 175 | } 176 | 177 | log.Infof("Enumerating %s DNS hosts", tools.FormatInt(total)) 178 | 179 | //Check DNS connectivity 180 | _, err := tools.GetValidDnsSuffix(fileOptions.DnsServer, "google.com.", opts.Proxy) 181 | if err != nil { 182 | log.Error("Error checking DNS connectivity", "err", err) 183 | os.Exit(2) 184 | } 185 | 186 | // Check runned items 187 | conn, _ := database.Connection(opts.Writer.CtrlDbURI, true, false) 188 | 189 | go func() { 190 | defer close(bruteRunner.Targets) 191 | 192 | ascii.HideCursor() 193 | for _, s := range dnsSuffix { 194 | bruteRunner.Targets <- s 195 | for _, h := range hostWordList { 196 | 197 | i := true 198 | host := h + "." + s 199 | if !forceCheck { 200 | response := conn.Raw("SELECT count(id) as count from results WHERE failed = 0 AND fqdn = ?", host) 201 | if response != nil { 202 | var cnt int 203 | _ = response.Row().Scan(&cnt) 204 | i = (cnt == 0) 205 | if cnt > 0 { 206 | log.Debug("[Host already checked]", "fqdn", host) 207 | } 208 | } 209 | } 210 | 211 | if i || forceCheck{ 212 | bruteRunner.Targets <- host 213 | }else{ 214 | bruteRunner.AddSkiped() 215 | } 216 | } 217 | } 218 | 219 | }() 220 | 221 | bruteRunner.Run(total) 222 | bruteRunner.Close() 223 | 224 | for _, writer := range resolveWriters { 225 | writer.Finish() 226 | } 227 | 228 | log.Info("Execution done!") 229 | }, 230 | } 231 | 232 | func init() { 233 | resolveCmd.AddCommand(resolveCrtshCmd) 234 | 235 | resolveCrtshCmd.Flags().StringVarP(&opts.DnsSuffix, "dns-suffix", "d", "", "Single DNS suffix. (ex: helviojunior.com.br)") 236 | resolveCrtshCmd.Flags().StringVarP(&fileOptions.DnsSuffixFile, "dns-list", "L", "", "File containing a list of DNS suffix") 237 | resolveCrtshCmd.Flags().StringVar(&fqdnOutFile, "fqdn-out", "", "Output file to save requested FQDN") 238 | } -------------------------------------------------------------------------------- /cmd/resolve_file.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | //"log/slog" 6 | "os" 7 | "strings" 8 | 9 | "github.com/helviojunior/enumdns/internal/ascii" 10 | "github.com/helviojunior/enumdns/internal/tools" 11 | "github.com/helviojunior/enumdns/pkg/log" 12 | //"github.com/helviojunior/enumdns/pkg/runner" 13 | "github.com/helviojunior/enumdns/pkg/database" 14 | "github.com/helviojunior/enumdns/pkg/writers" 15 | "github.com/helviojunior/enumdns/pkg/readers" 16 | "github.com/spf13/cobra" 17 | ) 18 | 19 | var resolveFileWriters = []writers.Writer{} 20 | var resolveFileCmd = &cobra.Command{ 21 | Use: "file", 22 | Short: "Perform resolve roperations", 23 | Long: ascii.LogoHelp(ascii.Markdown(` 24 | # resolve file 25 | 26 | Perform resolver operations. 27 | `)), 28 | Example: ` 29 | - enumdns resolve file -L /tmp/host_list.txt -o enumdns.txt 30 | - enumdns resolve file -L /tmp/host_list.txt --write-jsonl 31 | - enumdns resolve file -L /tmp/host_list.txt --write-db`, 32 | PersistentPreRunE: func(cmd *cobra.Command, args []string) error { 33 | // Annoying quirk, but because I'm overriding PersistentPreRun 34 | // here which overrides the parent it seems. 35 | // So we need to explicitly call the parent's one now. 36 | if err := resolveCmd.PersistentPreRunE(cmd, args); err != nil { 37 | return err 38 | } 39 | 40 | return nil 41 | }, 42 | PreRunE: func(cmd *cobra.Command, args []string) error { 43 | if fileOptions.HostFile == "" { 44 | return errors.New("a hosts list file must be specified") 45 | } 46 | 47 | if !tools.FileExists(fileOptions.HostFile) { 48 | return errors.New("hosts list file is not readable") 49 | } 50 | 51 | if err := resolveCmd.PreRunE(cmd, args); err != nil { 52 | return err 53 | } 54 | 55 | return nil 56 | }, 57 | Run: func(cmd *cobra.Command, args []string) { 58 | 59 | log.Debug("starting DNS resolver with file list") 60 | 61 | hostWordList := []string{} 62 | reader := readers.NewFileReader(fileOptions) 63 | total := 0 64 | 65 | log.Debugf("Reading dns hosts list file: %s", fileOptions.HostFile) 66 | if err := reader.ReadWordList(&hostWordList); err != nil { 67 | log.Error("error in reader.Read", "err", err) 68 | os.Exit(2) 69 | } 70 | total = len(hostWordList) 71 | 72 | if len(hostWordList) == 0 { 73 | log.Error("DNS host list is empty") 74 | os.Exit(2) 75 | } 76 | 77 | log.Infof("Enumerating %s DNS hosts", tools.FormatInt(total)) 78 | 79 | // Check runned items 80 | conn, _ := database.Connection(opts.Writer.CtrlDbURI, true, false) 81 | 82 | go func() { 83 | defer close(resolveRunner.Targets) 84 | 85 | ascii.HideCursor() 86 | 87 | for _, h := range hostWordList { 88 | 89 | i := true 90 | host := strings.Trim(h, ". ") + "." 91 | if !forceCheck { 92 | response := conn.Raw("SELECT count(id) as count from results WHERE failed = 0 AND fqdn = ?", host) 93 | if response != nil { 94 | var cnt int 95 | _ = response.Row().Scan(&cnt) 96 | i = (cnt == 0) 97 | if cnt > 0 { 98 | log.Debug("[Host already checked]", "fqdn", host) 99 | } 100 | } 101 | } 102 | 103 | if i || forceCheck{ 104 | resolveRunner.Targets <- host 105 | }else{ 106 | resolveRunner.AddSkiped() 107 | } 108 | } 109 | 110 | 111 | }() 112 | 113 | resolveRunner.Run(total) 114 | resolveRunner.Close() 115 | 116 | }, 117 | } 118 | 119 | func init() { 120 | resolveCmd.AddCommand(resolveFileCmd) 121 | 122 | resolveFileCmd.Flags().StringVarP(&fileOptions.HostFile, "host-list", "L", "", "File containing a list of DNS hosts") 123 | } -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | //"crypto/tls" 5 | "net/url" 6 | "os/user" 7 | "os" 8 | "fmt" 9 | "errors" 10 | "os/signal" 11 | "syscall" 12 | "time" 13 | 14 | "github.com/helviojunior/enumdns/internal" 15 | "github.com/helviojunior/enumdns/internal/tools" 16 | "github.com/helviojunior/enumdns/internal/ascii" 17 | "github.com/helviojunior/enumdns/pkg/log" 18 | "github.com/helviojunior/enumdns/pkg/runner" 19 | "github.com/helviojunior/enumdns/pkg/readers" 20 | resolver "github.com/helviojunior/gopathresolver" 21 | "github.com/spf13/cobra" 22 | ) 23 | 24 | var ( 25 | opts = &runner.Options{} 26 | fileOptions = &readers.FileReaderOptions{} 27 | tProxy = "" 28 | forceCheck = false 29 | tempFolder = "" 30 | ) 31 | 32 | var rootCmd = &cobra.Command{ 33 | Use: "enumdns", 34 | Short: "enumdns is a modular DNS recon tool", 35 | Long: ascii.Logo(), 36 | Example: ` 37 | - enumdns recon -d helviojunior.com.br -o enumdns.txt 38 | - enumdns recon -d helviojunior.com.br --write-jsonl 39 | - enumdns recon -L domains.txt --write-db 40 | 41 | - enumdns brute -d helviojunior.com.br -w /tmp/wordlist.txt -o enumdns.txt 42 | - enumdns brute -d helviojunior.com.br -w /tmp/wordlist.txt --write-jsonl 43 | - enumdns brute -L domains.txt -w /tmp/wordlist.txt --write-db 44 | 45 | - enumdns resolve bloodhound -L /tmp/bloodhound_computers.json -o enumdns.txt 46 | - enumdns resolve bloodhound -L /tmp/bloodhound_files.zip --write-jsonl 47 | - enumdns resolve bloodhound -L /tmp/bloodhound_computers.json --write-db 48 | 49 | - enumdns resolve file -L /tmp/host_list.txt -o enumdns.txt 50 | - enumdns resolve file -L /tmp/host_list.txt --write-jsonl 51 | - enumdns resolve file -L /tmp/host_list.txt --write-db`, 52 | PersistentPreRunE: func(cmd *cobra.Command, args []string) error { 53 | 54 | if cmd.CalledAs() != "version" && !opts.Logging.Silence { 55 | fmt.Println(ascii.Logo()) 56 | } 57 | 58 | if opts.Logging.Silence { 59 | log.EnableSilence() 60 | } 61 | 62 | if opts.Logging.Debug && !opts.Logging.Silence { 63 | log.EnableDebug() 64 | log.Debug("debug logging enabled") 65 | } 66 | 67 | 68 | usr, err := user.Current() 69 | if err != nil { 70 | return err 71 | } 72 | 73 | opts.Writer.UserPath = usr.HomeDir 74 | 75 | opts.Writer.CtrlDbURI = "sqlite:///" + opts.Writer.UserPath +"/.enumdns.db" 76 | 77 | basePath := "" 78 | if opts.StoreTempAsWorkspace { 79 | basePath = "./" 80 | } 81 | 82 | if tempFolder, err = tools.CreateDir(tools.TempFileName(basePath, "enumdns_", "")); err != nil { 83 | log.Error("error creatting temp folder", "err", err) 84 | os.Exit(2) 85 | } 86 | 87 | if opts.LocalWorkspace { 88 | tmp, err := resolver.ResolveFullPath("./enumdns_ctrl.db") 89 | if err != nil { 90 | return err 91 | } 92 | 93 | opts.Writer.CtrlDbURI = "sqlite:///" + tmp 94 | log.Info("Control DB", "Path", tmp) 95 | } 96 | 97 | if opts.Writer.NoControlDb { 98 | opts.Writer.CtrlDbURI = "sqlite:///"+ tools.TempFileName(tempFolder, "enumdns_", ".db") 99 | } 100 | 101 | if opts.Writer.TextFile != "" { 102 | 103 | opts.Writer.TextFile, err = resolver.ResolveFullPath(opts.Writer.TextFile) 104 | if err != nil { 105 | return err 106 | } 107 | 108 | opts.Writer.Text = true 109 | } 110 | 111 | if opts.DnsServer == "" { 112 | opts.DnsServer = tools.GetDefaultDnsServer("") 113 | } 114 | opts.PrivateDns = tools.IsPrivateIP(opts.DnsServer) 115 | 116 | fileOptions.DnsServer = opts.DnsServer + ":" + fmt.Sprintf("%d", opts.DnsPort) 117 | if opts.PrivateDns { 118 | log.Warnf("DNS server: %s (private)", fileOptions.DnsServer) 119 | }else{ 120 | log.Warn("DNS server: " + fileOptions.DnsServer) 121 | } 122 | 123 | //Check Proxy config 124 | if tProxy != "" { 125 | u, err := url.Parse(tProxy) 126 | if err != nil { 127 | return errors.New("Error parsing URL: " + err.Error()) 128 | } 129 | 130 | _, err = internal.FromURL(u, nil) 131 | if err != nil { 132 | return errors.New("Error parsing URL: " + err.Error()) 133 | } 134 | opts.Proxy = u 135 | fileOptions.ProxyUri = opts.Proxy 136 | 137 | port := u.Port() 138 | if port == "" { 139 | port = "1080" 140 | } 141 | log.Warn("Setting proxy to " + u.Scheme + "://" + u.Hostname() + ":" + port) 142 | }else{ 143 | opts.Proxy = nil 144 | } 145 | 146 | return nil 147 | }, 148 | } 149 | 150 | func Execute() { 151 | 152 | ascii.SetConsoleColors() 153 | 154 | c := make(chan os.Signal) 155 | signal.Notify(c, os.Interrupt, syscall.SIGTERM) 156 | go func() { 157 | <-c 158 | ascii.ClearLine() 159 | fmt.Fprintf(os.Stderr, "\r\n") 160 | ascii.ClearLine() 161 | ascii.ShowCursor() 162 | log.Warn("interrupted, shutting down... ") 163 | ascii.ClearLine() 164 | fmt.Printf("\n") 165 | tools.RemoveFolder(tempFolder) 166 | os.Exit(2) 167 | }() 168 | 169 | rootCmd.CompletionOptions.DisableDefaultCmd = true 170 | rootCmd.SilenceErrors = true 171 | err := rootCmd.Execute() 172 | if err != nil { 173 | var cmd string 174 | c, _, cerr := rootCmd.Find(os.Args[1:]) 175 | if cerr == nil { 176 | cmd = c.Name() 177 | } 178 | 179 | v := "\n" 180 | 181 | if cmd != "" { 182 | v += fmt.Sprintf("An error occured running the `%s` command\n", cmd) 183 | } else { 184 | v += "An error has occured. " 185 | } 186 | 187 | v += "The error was:\n\n" + fmt.Sprintf("```%s```", err) 188 | fmt.Println(ascii.Markdown(v)) 189 | 190 | os.Exit(1) 191 | } 192 | 193 | //Time to wait the logger flush 194 | time.Sleep(time.Second/4) 195 | tools.RemoveFolder(tempFolder) 196 | ascii.ShowCursor() 197 | fmt.Printf("\n") 198 | } 199 | 200 | func init() { 201 | // Disable Certificate Validation (Globally) 202 | //http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} 203 | 204 | rootCmd.PersistentFlags().StringVarP(&opts.DnsServer, "server", "s", "", "DNS Server") 205 | rootCmd.PersistentFlags().IntVar(&opts.DnsPort, "port", 53, "DNS Server Port") 206 | rootCmd.PersistentFlags().StringVarP(&opts.DnsProtocol, "protocol", "", "UDP", "DNS Server protocol (TCP/UDP)") 207 | 208 | rootCmd.PersistentFlags().BoolVarP(&opts.Logging.Debug, "debug-log", "D", false, "Enable debug logging") 209 | rootCmd.PersistentFlags().BoolVarP(&opts.Logging.Silence, "quiet", "q", false, "Silence (almost all) logging") 210 | rootCmd.PersistentFlags().BoolVarP(&forceCheck, "force", "F", false, "Force to check all hosts again.") 211 | 212 | // Logging control for subcommands 213 | rootCmd.PersistentFlags().BoolVar(&opts.Logging.LogScanErrors, "log-scan-errors", false, "Log scan errors (timeouts, DNS errors, etc.) to stderr (warning: can be verbose!)") 214 | 215 | rootCmd.PersistentFlags().StringVarP(&opts.Writer.TextFile, "write-text-file", "o", "", "The file to write Text lines to") 216 | 217 | 218 | //rootCmd.PersistentFlags().BoolVarP(&opts.DnsOverHttps.SkipSSLCheck, "ssl-insecure", "K", true, "SSL Insecure") 219 | rootCmd.PersistentFlags().StringVarP(&tProxy, "proxy", "X", "", "Proxy to pass traffic through: (e.g., socks4://user:pass@proxy_host:1080") 220 | //rootCmd.PersistentFlags().StringVarP(&opts.DnsOverHttps.ProxyUser, "proxy-user", "", "", "Proxy User") 221 | //rootCmd.PersistentFlags().StringVarP(&opts.DnsOverHttps.ProxyPassword, "proxy-pass", "", "", "Proxy Password") 222 | 223 | // "Threads" & other 224 | rootCmd.PersistentFlags().IntVarP(&opts.Scan.Threads, "threads", "t", 6, "Number of concurrent threads (goroutines) to use") 225 | rootCmd.PersistentFlags().IntVarP(&opts.Scan.Timeout, "timeout", "T", 60, "Number of seconds before considering a page timed out") 226 | 227 | rootCmd.PersistentFlags().BoolVar(&opts.Writer.NoControlDb, "disable-control-db", false, "Disable utilization of database ~/.enumdns.db.") 228 | rootCmd.PersistentFlags().BoolVar(&opts.StoreTempAsWorkspace, "local-temp", false, "Use execution path to store temp files") 229 | rootCmd.PersistentFlags().BoolVar(&opts.LocalWorkspace, "local-workspace", false, "Use execution path to store .enumdns.db intead of user home") 230 | 231 | // Write options for scan subcommands 232 | rootCmd.PersistentFlags().BoolVar(&opts.Writer.Db, "write-db", false, "Write results to a SQLite database") 233 | rootCmd.PersistentFlags().StringVar(&opts.Writer.DbURI, "write-db-uri", "sqlite://enumdns.sqlite3", "The database URI to use. Supports SQLite, Postgres, and MySQL (e.g., postgres://user:pass@host:port/db)") 234 | rootCmd.PersistentFlags().BoolVar(&opts.Writer.DbDebug, "write-db-enable-debug", false, "Enable database query debug logging (warning: verbose!)") 235 | rootCmd.PersistentFlags().BoolVar(&opts.Writer.Csv, "write-csv", false, "Write results as CSV (has limited columns)") 236 | rootCmd.PersistentFlags().StringVar(&opts.Writer.CsvFile, "write-csv-file", "enumdns.csv", "The file to write CSV rows to") 237 | rootCmd.PersistentFlags().BoolVar(&opts.Writer.Jsonl, "write-jsonl", false, "Write results as JSON lines") 238 | rootCmd.PersistentFlags().StringVar(&opts.Writer.JsonlFile, "write-jsonl-file", "enumdns.jsonl", "The file to write JSON lines to") 239 | rootCmd.PersistentFlags().BoolVar(&opts.Writer.None, "write-none", false, "Use an empty writer to silence warnings") 240 | 241 | rootCmd.PersistentFlags().BoolVar(&opts.Writer.ELastic, "write-elastic", false, "Write results to a SQLite database") 242 | rootCmd.PersistentFlags().StringVar(&opts.Writer.ELasticURI, "write-elasticsearch-uri", "http://localhost:9200/enumdns", "The elastic search URI to use. (e.g., http://user:pass@host:9200/index)") 243 | 244 | 245 | } 246 | -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/helviojunior/enumdns/internal/ascii" 7 | "github.com/helviojunior/enumdns/internal/version" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | var versionCmd = &cobra.Command{ 12 | Use: "version", 13 | Short: "Get the enumdns version", 14 | Long: ascii.LogoHelp(`Get the enumdns version.`), 15 | Run: func(cmd *cobra.Command, args []string) { 16 | fmt.Println(ascii.Logo()) 17 | 18 | fmt.Println("Author: Helvio Junior (m4v3r1ck)") 19 | fmt.Println("Source: https://github.com/helviojunior/enumdns") 20 | fmt.Printf("Version: %s\nGit hash: %s\nBuild env: %s\nBuild time: %s\n\n", 21 | version.Version, version.GitHash, version.GoBuildEnv, version.GoBuildTime) 22 | }, 23 | } 24 | 25 | func init() { 26 | rootCmd.AddCommand(versionCmd) 27 | } -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/helviojunior/enumdns 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/charmbracelet/glamour v0.8.0 7 | github.com/charmbracelet/lipgloss v0.12.1 8 | github.com/charmbracelet/log v0.4.0 9 | github.com/elastic/go-elasticsearch/v8 v8.17.0 10 | github.com/glebarez/sqlite v1.11.0 11 | github.com/helviojunior/gopathresolver v0.1.0 12 | github.com/miekg/dns v1.1.63 13 | github.com/prometheus/procfs v0.16.0 14 | github.com/spf13/cobra v1.8.1 15 | golang.org/x/net v0.31.0 16 | golang.org/x/sys v0.30.0 17 | golang.org/x/term v0.26.0 18 | gorm.io/driver/mysql v1.5.7 19 | gorm.io/driver/postgres v1.5.11 20 | gorm.io/gorm v1.30.0 21 | ) 22 | 23 | require ( 24 | github.com/alecthomas/chroma/v2 v2.14.0 // indirect 25 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect 26 | github.com/aymerick/douceur v0.2.0 // indirect 27 | github.com/charmbracelet/x/ansi v0.1.4 // indirect 28 | github.com/dlclark/regexp2 v1.11.0 // indirect 29 | github.com/dustin/go-humanize v1.0.1 // indirect 30 | github.com/elastic/elastic-transport-go/v8 v8.6.0 // indirect 31 | github.com/glebarez/go-sqlite v1.21.2 // indirect 32 | github.com/go-logfmt/logfmt v0.6.0 // indirect 33 | github.com/go-logr/logr v1.4.2 // indirect 34 | github.com/go-logr/stdr v1.2.2 // indirect 35 | github.com/go-sql-driver/mysql v1.7.0 // indirect 36 | github.com/google/uuid v1.3.0 // indirect 37 | github.com/gorilla/css v1.0.1 // indirect 38 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 39 | github.com/jackc/pgpassfile v1.0.0 // indirect 40 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect 41 | github.com/jackc/pgx/v5 v5.5.5 // indirect 42 | github.com/jackc/puddle/v2 v2.2.1 // indirect 43 | github.com/jinzhu/inflection v1.0.0 // indirect 44 | github.com/jinzhu/now v1.1.5 // indirect 45 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 46 | github.com/mattn/go-isatty v0.0.20 // indirect 47 | github.com/mattn/go-runewidth v0.0.15 // indirect 48 | github.com/microcosm-cc/bluemonday v1.0.27 // indirect 49 | github.com/muesli/reflow v0.3.0 // indirect 50 | github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a // indirect 51 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect 52 | github.com/rivo/uniseg v0.4.7 // indirect 53 | github.com/spf13/pflag v1.0.5 // indirect 54 | github.com/yuin/goldmark v1.7.4 // indirect 55 | github.com/yuin/goldmark-emoji v1.0.3 // indirect 56 | go.opentelemetry.io/otel v1.28.0 // indirect 57 | go.opentelemetry.io/otel/metric v1.28.0 // indirect 58 | go.opentelemetry.io/otel/trace v1.28.0 // indirect 59 | golang.org/x/crypto v0.29.0 // indirect 60 | golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect 61 | golang.org/x/mod v0.18.0 // indirect 62 | golang.org/x/sync v0.11.0 // indirect 63 | golang.org/x/text v0.20.0 // indirect 64 | golang.org/x/tools v0.22.0 // indirect 65 | modernc.org/libc v1.22.5 // indirect 66 | modernc.org/mathutil v1.5.0 // indirect 67 | modernc.org/memory v1.5.0 // indirect 68 | modernc.org/sqlite v1.23.1 // indirect 69 | ) 70 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE= 2 | github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= 3 | github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E= 4 | github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I= 5 | github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= 6 | github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= 7 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= 8 | github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= 9 | github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= 10 | github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= 11 | github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= 12 | github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= 13 | github.com/charmbracelet/glamour v0.8.0 h1:tPrjL3aRcQbn++7t18wOpgLyl8wrOHUEDS7IZ68QtZs= 14 | github.com/charmbracelet/glamour v0.8.0/go.mod h1:ViRgmKkf3u5S7uakt2czJ272WSg2ZenlYEZXT2x7Bjw= 15 | github.com/charmbracelet/lipgloss v0.12.1 h1:/gmzszl+pedQpjCOH+wFkZr/N90Snz40J/NR7A0zQcs= 16 | github.com/charmbracelet/lipgloss v0.12.1/go.mod h1:V2CiwIuhx9S1S1ZlADfOj9HmxeMAORuz5izHb0zGbB8= 17 | github.com/charmbracelet/log v0.4.0 h1:G9bQAcx8rWA2T3pWvx7YtPTPwgqpk7D68BX21IRW8ZM= 18 | github.com/charmbracelet/log v0.4.0/go.mod h1:63bXt/djrizTec0l11H20t8FDSvA4CRZJ1KH22MdptM= 19 | github.com/charmbracelet/x/ansi v0.1.4 h1:IEU3D6+dWwPSgZ6HBH+v6oUuZ/nVawMiWj5831KfiLM= 20 | github.com/charmbracelet/x/ansi v0.1.4/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= 21 | github.com/charmbracelet/x/exp/golden v0.0.0-20240715153702-9ba8adf781c4 h1:6KzMkQeAF56rggw2NZu1L+TH7j9+DM1/2Kmh7KUxg1I= 22 | github.com/charmbracelet/x/exp/golden v0.0.0-20240715153702-9ba8adf781c4/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= 23 | github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 24 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 25 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 26 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 27 | github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= 28 | github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= 29 | github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= 30 | github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 31 | github.com/elastic/elastic-transport-go/v8 v8.6.0 h1:Y2S/FBjx1LlCv5m6pWAF2kDJAHoSjSRSJCApolgfthA= 32 | github.com/elastic/elastic-transport-go/v8 v8.6.0/go.mod h1:YLHer5cj0csTzNFXoNQ8qhtGY1GTvSqPnKWKaqQE3Hk= 33 | github.com/elastic/go-elasticsearch/v8 v8.17.0 h1:e9cWksE/Fr7urDRmGPGp47Nsp4/mvNOrU8As1l2HQQ0= 34 | github.com/elastic/go-elasticsearch/v8 v8.17.0/go.mod h1:lGMlgKIbYoRvay3xWBeKahAiJOgmFDsjZC39nmO3H64= 35 | github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo= 36 | github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k= 37 | github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw= 38 | github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= 39 | github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= 40 | github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= 41 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 42 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 43 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 44 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 45 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 46 | github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= 47 | github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= 48 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 49 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 50 | github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= 51 | github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= 52 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 53 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 54 | github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= 55 | github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= 56 | github.com/helviojunior/gopathresolver v0.1.0 h1:UwDTt+pptvwaHfV3pZ74Z2lNf1P6PW+BfawLR4TaD0s= 57 | github.com/helviojunior/gopathresolver v0.1.0/go.mod h1:19ixd6gL/i7Md6lyoM4mPGgZZ3/xQsEHVqn3dhreads= 58 | github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= 59 | github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= 60 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 61 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 62 | github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= 63 | github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= 64 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= 65 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= 66 | github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw= 67 | github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= 68 | github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= 69 | github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= 70 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 71 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 72 | github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= 73 | github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 74 | github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= 75 | github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 76 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 77 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 78 | github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= 79 | github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= 80 | github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 81 | github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= 82 | github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= 83 | github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY= 84 | github.com/miekg/dns v1.1.63/go.mod h1:6NGHfjhpmr5lt3XPLuyfDJi5AXbNIPM9PY6H6sF1Nfs= 85 | github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= 86 | github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= 87 | github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a h1:2MaM6YC3mGu54x+RKAA6JiFFHlHDY1UbkxqppT7wYOg= 88 | github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a/go.mod h1:hxSnBBYLK21Vtq/PHd0S2FYCxBXzBua8ov5s1RobyRQ= 89 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 90 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 91 | github.com/prometheus/procfs v0.16.0 h1:xh6oHhKwnOJKMYiYBDWmkHqQPyiY40sny36Cmx2bbsM= 92 | github.com/prometheus/procfs v0.16.0/go.mod h1:8veyXUu3nGP7oaCxhX6yeaM5u4stL2FeMXnCqhDthZg= 93 | github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= 94 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= 95 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= 96 | github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 97 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 98 | github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= 99 | github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 100 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 101 | github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= 102 | github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= 103 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 104 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 105 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 106 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 107 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 108 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 109 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 110 | github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= 111 | github.com/yuin/goldmark v1.7.4 h1:BDXOHExt+A7gwPCJgPIIq7ENvceR7we7rOS9TNoLZeg= 112 | github.com/yuin/goldmark v1.7.4/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= 113 | github.com/yuin/goldmark-emoji v1.0.3 h1:aLRkLHOuBR2czCY4R8olwMjID+tENfhyFDMCRhbIQY4= 114 | github.com/yuin/goldmark-emoji v1.0.3/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U= 115 | go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo= 116 | go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4= 117 | go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q= 118 | go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s= 119 | go.opentelemetry.io/otel/sdk v1.21.0 h1:FTt8qirL1EysG6sTQRZ5TokkU8d0ugCj8htOgThZXQ8= 120 | go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E= 121 | go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g= 122 | go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI= 123 | golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= 124 | golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= 125 | golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= 126 | golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= 127 | golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= 128 | golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 129 | golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= 130 | golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= 131 | golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= 132 | golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 133 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 134 | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 135 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 136 | golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU= 137 | golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E= 138 | golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= 139 | golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= 140 | golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= 141 | golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= 142 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 143 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 144 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 145 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 146 | gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo= 147 | gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM= 148 | gorm.io/driver/postgres v1.5.11 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314= 149 | gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI= 150 | gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= 151 | gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs= 152 | gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= 153 | modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE= 154 | modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY= 155 | modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= 156 | modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= 157 | modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds= 158 | modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= 159 | modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM= 160 | modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk= 161 | -------------------------------------------------------------------------------- /internal/ascii/ansi.go: -------------------------------------------------------------------------------- 1 | package ascii 2 | 3 | import ( 4 | "regexp" 5 | ) 6 | 7 | var ansiPattern *regexp.Regexp = regexp.MustCompile(`(\x9B|\x1B\[)[0-?]*[ -/]*[@-~]`) 8 | 9 | func ScapeAnsi(text string) string { 10 | return ansiPattern.ReplaceAllString(text, "") 11 | } 12 | -------------------------------------------------------------------------------- /internal/ascii/colors_other.go: -------------------------------------------------------------------------------- 1 | /* 2 | * PACP - PCAP manipulation tool in Golang 3 | * Copyright (c) 2025 Helvio Junior 4 | * 5 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 6 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 7 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 8 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 9 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 10 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 11 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 12 | */ 13 | 14 | //go:build !windows 15 | 16 | package ascii 17 | 18 | func SetConsoleColors() error { 19 | return nil 20 | } -------------------------------------------------------------------------------- /internal/ascii/colors_windows.go: -------------------------------------------------------------------------------- 1 | /* 2 | * PACP - PCAP manipulation tool in Golang 3 | * Copyright (c) 2025 Helvio Junior 4 | * 5 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 6 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 7 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 8 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 9 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 10 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 11 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 12 | */ 13 | 14 | //go:build windows 15 | 16 | package ascii 17 | 18 | import "golang.org/x/sys/windows" 19 | 20 | func SetConsoleColors() error { 21 | console := windows.Stdout 22 | var consoleMode uint32 23 | windows.GetConsoleMode(console, &consoleMode) 24 | consoleMode |= windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING 25 | return windows.SetConsoleMode(console, consoleMode) 26 | } -------------------------------------------------------------------------------- /internal/ascii/console.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package ascii 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | ) 10 | 11 | // Show the cursor if it was hidden previously. 12 | // Don't forget to show the cursor at least at the end of your application. 13 | // Otherwise the user might have a terminal with a permanently hidden cursor, until they reopen the terminal. 14 | func ShowCursor() { 15 | fmt.Fprint(os.Stderr, "\x1b[?25h") 16 | } 17 | 18 | // Hide the cursor. 19 | // Don't forget to show the cursor at least at the end of your application with Show. 20 | // Otherwise the user might have a terminal with a permanently hidden cursor, until they reopen the terminal. 21 | func HideCursor() { 22 | fmt.Fprintf(os.Stderr, "\x1b[?25l") 23 | } 24 | 25 | // ClearLine clears the current line and moves the cursor to it's start position. 26 | func ClearLine() { 27 | fmt.Fprintf(os.Stderr, "\x1b[2K") 28 | } 29 | 30 | // Clear clears the current position and moves the cursor to the left. 31 | func Clear() { 32 | fmt.Fprintf(os.Stderr, "\x1b[K") 33 | } -------------------------------------------------------------------------------- /internal/ascii/console_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package ascii 5 | 6 | import ( 7 | "syscall" 8 | "unsafe" 9 | "os" 10 | ) 11 | 12 | var ( 13 | kernel32 = syscall.NewLazyDLL("kernel32.dll") 14 | procFillConsoleOutputCharacter = kernel32.NewProc("FillConsoleOutputCharacterW") 15 | procGetConsoleCursorInfo = kernel32.NewProc("GetConsoleCursorInfo") 16 | procGetConsoleScreenBufferInfo = kernel32.NewProc("GetConsoleScreenBufferInfo") 17 | procSetConsoleCursorInfo = kernel32.NewProc("SetConsoleCursorInfo") 18 | procSetConsoleCursorPosition = kernel32.NewProc("SetConsoleCursorPosition") 19 | ) 20 | 21 | type short int16 22 | type dword uint32 23 | type word uint16 24 | 25 | type coord struct { 26 | x short 27 | y short 28 | } 29 | 30 | type smallRect struct { 31 | bottom short 32 | left short 33 | right short 34 | top short 35 | } 36 | 37 | type consoleScreenBufferInfo struct { 38 | size coord 39 | cursorPosition coord 40 | attributes word 41 | window smallRect 42 | maximumWindowSize coord 43 | } 44 | 45 | type consoleCursorInfo struct { 46 | size dword 47 | visible int32 48 | } 49 | 50 | // Show the cursor if it was hidden previously. 51 | // Don't forget to show the cursor at least at the end of your application. 52 | // Otherwise the user might have a terminal with a permanently hidden cursor, until he reopens the terminal. 53 | func ShowCursor() { 54 | handle := syscall.Handle(os.Stderr.Fd()) 55 | 56 | var cci consoleCursorInfo 57 | _, _, _ = procGetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&cci))) 58 | cci.visible = 1 59 | 60 | _, _, _ = procSetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&cci))) 61 | } 62 | 63 | // Hide the cursor. 64 | // Don't forget to show the cursor at least at the end of your application with Show. 65 | // Otherwise the user might have a terminal with a permanently hidden cursor, until he reopens the terminal. 66 | func HideCursor() { 67 | handle := syscall.Handle(os.Stderr.Fd()) 68 | 69 | var cci consoleCursorInfo 70 | _, _, _ = procGetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&cci))) 71 | cci.visible = 0 72 | 73 | _, _, _ = procSetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&cci))) 74 | } 75 | 76 | // ClearLine clears the current line and moves the cursor to its start position. 77 | func ClearLine() { 78 | handle := syscall.Handle(os.Stderr.Fd()) 79 | 80 | var csbi consoleScreenBufferInfo 81 | _, _, _ = procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))) 82 | 83 | var w uint32 84 | var x short 85 | cursor := csbi.cursorPosition 86 | x = csbi.size.x 87 | _, _, _ = procFillConsoleOutputCharacter.Call(uintptr(handle), uintptr(' '), uintptr(x), uintptr(*(*int32)(unsafe.Pointer(&cursor))), uintptr(unsafe.Pointer(&w))) 88 | } 89 | 90 | // Clear clears the current position and moves the cursor to the left. 91 | func Clear() { 92 | handle := syscall.Handle(os.Stderr.Fd()) 93 | 94 | var csbi consoleScreenBufferInfo 95 | _, _, _ = procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))) 96 | 97 | var w uint32 98 | cursor := csbi.cursorPosition 99 | _, _, _ = procFillConsoleOutputCharacter.Call(uintptr(handle), uintptr(' '), uintptr(1), uintptr(*(*int32)(unsafe.Pointer(&cursor))), uintptr(unsafe.Pointer(&w))) 100 | 101 | if cursor.x > 0 { 102 | cursor.x-- 103 | } 104 | _, _, _ = procSetConsoleCursorPosition.Call(uintptr(handle), uintptr(*(*int32)(unsafe.Pointer(&cursor)))) 105 | } -------------------------------------------------------------------------------- /internal/ascii/logo.go: -------------------------------------------------------------------------------- 1 | package ascii 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "github.com/helviojunior/enumdns/internal/version" 7 | ) 8 | 9 | // Logo returns the enumdns ascii logo 10 | func Logo() string { 11 | txt := ` 12 | 13 | {G} ______ {O} ____ _ _______ 14 | {G} / ____/___ __ ______ ___ {O} / __ \/ | / / ___/ 15 | {G} / __/ / __ \/ / / / __ '__ \{O}/ / / / |/ /\__ \ 16 | {G} / /___/ / / / /_/ / / / / / /{O} /_/ / /| /___/ / 17 | {G}/_____/_/ /_/\__,_/_/ /_/ /_/{O}_____/_/ |_//____/ {B} 18 | ` 19 | 20 | v := fmt.Sprintf("Ver: %s-%s", version.Version, version.GitHash) 21 | txt += strings.Repeat(" ", 46 - len(v)) 22 | txt += v + "{W}" 23 | txt = strings.Replace(txt, "{G}", "\033[32m", -1) 24 | txt = strings.Replace(txt, "{B}", "\033[36m", -1) 25 | txt = strings.Replace(txt, "{O}", "\033[33m", -1) 26 | txt = strings.Replace(txt, "{W}", "\033[0m", -1) 27 | return fmt.Sprintln(txt) 28 | } 29 | 30 | // LogoHelp returns the logo, with help 31 | func LogoHelp(s string) string { 32 | return Logo() + "\n\n" + s 33 | } 34 | -------------------------------------------------------------------------------- /internal/ascii/markdown.go: -------------------------------------------------------------------------------- 1 | package ascii 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/charmbracelet/glamour" 7 | ) 8 | 9 | var renderer *glamour.TermRenderer 10 | 11 | // Markdown renders markdown 12 | func Markdown(s string) string { 13 | r, err := renderer.Render(strings.TrimSpace(s)) 14 | if err != nil { 15 | panic(err) 16 | } 17 | 18 | return r 19 | } 20 | 21 | func init() { 22 | var err error 23 | renderer, err = glamour.NewTermRenderer( 24 | glamour.WithAutoStyle(), 25 | glamour.WithPreservedNewLines(), 26 | ) 27 | if err != nil { 28 | panic(err) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /internal/ascii/spinner.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | 3 | package ascii 4 | 5 | func GetNextSpinner(spin string) string { 6 | chars := []string{ 7 | "⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏", //"⠿", 8 | } 9 | 10 | if spin == "" || spin == "⠿" { 11 | return chars[0] 12 | } 13 | 14 | for idx, e := range chars { 15 | if spin == e { 16 | if idx + 1 >= len(chars) { 17 | return chars[0] 18 | }else { 19 | return chars[idx + 1] 20 | } 21 | } 22 | } 23 | 24 | return "⠿" 25 | } 26 | 27 | func ColoredSpin(spin string) string { 28 | return "\033[36m" + spin + "\033[0m" 29 | } -------------------------------------------------------------------------------- /internal/ascii/spinner_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package ascii 4 | 5 | func GetNextSpinner(spin string) string { 6 | switch spin { 7 | case "[=====]": 8 | return "[ ====]" 9 | case "[ ====]": 10 | return "[ ===]" 11 | case "[ ===]": 12 | return "[= ==]" 13 | case "[= ==]": 14 | return "[== =]" 15 | case "[== =]": 16 | return "[=== ]" 17 | case "[=== ]": 18 | return "[==== ]" 19 | default: 20 | return "[=====]" 21 | } 22 | } 23 | 24 | func ColoredSpin(spin string) string { 25 | return spin 26 | } -------------------------------------------------------------------------------- /internal/disk/disk.go: -------------------------------------------------------------------------------- 1 | package disk 2 | 3 | // Info stat fs struct is container which holds following values 4 | // Total - total size of the volume / disk 5 | // Free - free size of the volume / disk 6 | // Files - total inodes available 7 | // Ffree - free inodes available 8 | // FSType - file system type 9 | // Major - major dev id 10 | // Minor - minor dev id 11 | // Devname - device name 12 | type Info struct { 13 | Total uint64 14 | Free uint64 15 | Used uint64 16 | Files uint64 17 | Ffree uint64 18 | FSType string 19 | Major uint32 20 | Minor uint32 21 | Name string 22 | Rotational *bool 23 | NRRequests uint64 24 | } 25 | 26 | -------------------------------------------------------------------------------- /internal/disk/disk_bsd.go: -------------------------------------------------------------------------------- 1 | //go:build darwin || dragonfly 2 | // +build darwin dragonfly 3 | 4 | package disk 5 | 6 | import ( 7 | "fmt" 8 | "syscall" 9 | ) 10 | 11 | // GetInfo returns total and free bytes available in a directory, e.g. `/`. 12 | func GetInfo(path string, _ bool) (info Info, err error) { 13 | s := syscall.Statfs_t{} 14 | err = syscall.Statfs(path, &s) 15 | if err != nil { 16 | return Info{}, err 17 | } 18 | reservedBlocks := s.Bfree - s.Bavail 19 | info = Info{ 20 | Total: uint64(s.Bsize) * (s.Blocks - reservedBlocks), 21 | Free: uint64(s.Bsize) * s.Bavail, 22 | Files: s.Files, 23 | Ffree: s.Ffree, 24 | FSType: getFSType(s.Fstypename[:]), 25 | } 26 | if info.Free > info.Total { 27 | return info, fmt.Errorf("detected free space (%d) > total drive space (%d), fs corruption at (%s). please run 'fsck'", info.Free, info.Total, path) 28 | } 29 | info.Used = info.Total - info.Free 30 | return info, nil 31 | } 32 | 33 | 34 | // getFSType returns the filesystem type of the underlying mounted filesystem 35 | func getFSType(fstype []int8) string { 36 | b := make([]byte, len(fstype)) 37 | for i, v := range fstype { 38 | b[i] = byte(v) 39 | } 40 | return string(b) 41 | } -------------------------------------------------------------------------------- /internal/disk/disk_unix.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | // +build linux 3 | 4 | package disk 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | "path/filepath" 10 | "strings" 11 | "syscall" 12 | 13 | "strconv" 14 | 15 | "github.com/prometheus/procfs/blockdevice" 16 | "golang.org/x/sys/unix" 17 | ) 18 | 19 | 20 | // fsType2StringMap - list of filesystems supported on linux 21 | var fsType2StringMap = map[string]string{ 22 | "1021994": "TMPFS", 23 | "137d": "EXT", 24 | "4244": "HFS", 25 | "4d44": "MSDOS", 26 | "52654973": "REISERFS", 27 | "5346544e": "NTFS", 28 | "58465342": "XFS", 29 | "61756673": "AUFS", 30 | "6969": "NFS", 31 | "ef51": "EXT2OLD", 32 | "ef53": "EXT4", 33 | "f15f": "ecryptfs", 34 | "794c7630": "overlayfs", 35 | "2fc12fc1": "zfs", 36 | "ff534d42": "cifs", 37 | "53464846": "wslfs", 38 | } 39 | 40 | // GetInfo returns total and free bytes available in a directory, e.g. `/`. 41 | func GetInfo(path string, firstTime bool) (info Info, err error) { 42 | s := syscall.Statfs_t{} 43 | err = syscall.Statfs(path, &s) 44 | if err != nil { 45 | return Info{}, err 46 | } 47 | reservedBlocks := s.Bfree - s.Bavail 48 | info = Info{ 49 | Total: uint64(s.Frsize) * (s.Blocks - reservedBlocks), 50 | Free: uint64(s.Frsize) * s.Bavail, 51 | Files: s.Files, 52 | Ffree: s.Ffree, 53 | //nolint:unconvert 54 | FSType: getFSType(uint32(s.Type)), 55 | } 56 | 57 | st := syscall.Stat_t{} 58 | err = syscall.Stat(path, &st) 59 | if err != nil { 60 | return Info{}, err 61 | } 62 | //nolint:unconvert 63 | devID := uint64(st.Dev) // Needed to support multiple GOARCHs 64 | info.Major = unix.Major(devID) 65 | info.Minor = unix.Minor(devID) 66 | 67 | // Check for overflows. 68 | // https://github.com/minio/minio/issues/8035 69 | // XFS can show wrong values at times error out 70 | // in such scenarios. 71 | if info.Free > info.Total { 72 | return info, fmt.Errorf("detected free space (%d) > total drive space (%d), fs corruption at (%s). please run 'fsck'", info.Free, info.Total, path) 73 | } 74 | info.Used = info.Total - info.Free 75 | 76 | if firstTime { 77 | bfs, err := blockdevice.NewDefaultFS() 78 | if err == nil { 79 | devName := "" 80 | diskstats, _ := bfs.ProcDiskstats() 81 | for _, dstat := range diskstats { 82 | // ignore all loop devices 83 | if strings.HasPrefix(dstat.DeviceName, "loop") { 84 | continue 85 | } 86 | if dstat.MajorNumber == info.Major && dstat.MinorNumber == info.Minor { 87 | devName = dstat.DeviceName 88 | break 89 | } 90 | } 91 | if devName != "" { 92 | info.Name = devName 93 | qst, err := bfs.SysBlockDeviceQueueStats(devName) 94 | if err != nil { // Mostly not found error 95 | // Check if there is a parent device: 96 | // e.g. if the mount is based on /dev/nvme0n1p1, let's calculate the 97 | // real device name (nvme0n1) to get its sysfs information 98 | parentDevPath, e := os.Readlink("/sys/class/block/" + devName) 99 | if e == nil { 100 | parentDev := filepath.Base(filepath.Dir(parentDevPath)) 101 | qst, err = bfs.SysBlockDeviceQueueStats(parentDev) 102 | } 103 | } 104 | if err == nil { 105 | info.NRRequests = qst.NRRequests 106 | rot := qst.Rotational == 1 // Rotational is '1' if the device is HDD 107 | info.Rotational = &rot 108 | } 109 | } 110 | } 111 | } 112 | 113 | return info, nil 114 | } 115 | 116 | 117 | // getFSType returns the filesystem type of the underlying mounted filesystem 118 | func getFSType(ftype uint32) string { 119 | fsTypeHex := strconv.FormatUint(uint64(ftype), 16) 120 | fsTypeString, ok := fsType2StringMap[fsTypeHex] 121 | if !ok { 122 | return "UNKNOWN" 123 | } 124 | return fsTypeString 125 | } 126 | -------------------------------------------------------------------------------- /internal/disk/disk_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package disk 5 | 6 | import ( 7 | "path/filepath" 8 | "fmt" 9 | "os" 10 | "syscall" 11 | "unsafe" 12 | 13 | "golang.org/x/sys/windows" 14 | ) 15 | 16 | var ( 17 | kernel32 = windows.NewLazySystemDLL("kernel32.dll") 18 | 19 | // GetDiskFreeSpaceEx - https://msdn.microsoft.com/en-us/library/windows/desktop/aa364937(v=vs.85).aspx 20 | // Retrieves information about the amount of space that is available on a disk volume, 21 | // which is the total amount of space, the total amount of free space, and the total 22 | // amount of free space available to the user that is associated with the calling thread. 23 | GetDiskFreeSpaceEx = kernel32.NewProc("GetDiskFreeSpaceExW") 24 | 25 | // GetDiskFreeSpace - https://msdn.microsoft.com/en-us/library/windows/desktop/aa364935(v=vs.85).aspx 26 | // Retrieves information about the specified disk, including the amount of free space on the disk. 27 | GetDiskFreeSpace = kernel32.NewProc("GetDiskFreeSpaceW") 28 | 29 | // GetVolumeInformation provides windows drive volume information. 30 | GetVolumeInformation = kernel32.NewProc("GetVolumeInformationW") 31 | ) 32 | 33 | // GetInfo returns total and free bytes available in a directory, e.g. `C:\`. 34 | // It returns free space available to the user (including quota limitations) 35 | // 36 | // https://msdn.microsoft.com/en-us/library/windows/desktop/aa364937(v=vs.85).aspx 37 | func GetInfo(path string, _ bool) (info Info, err error) { 38 | // Stat to know if the path exists. 39 | if _, err = os.Stat(path); err != nil { 40 | return Info{}, err 41 | } 42 | 43 | lpFreeBytesAvailable := int64(0) 44 | lpTotalNumberOfBytes := int64(0) 45 | lpTotalNumberOfFreeBytes := int64(0) 46 | 47 | // Extract values safely 48 | // BOOL WINAPI GetDiskFreeSpaceEx( 49 | // _In_opt_ LPCTSTR lpDirectoryName, 50 | // _Out_opt_ PULARGE_INTEGER lpFreeBytesAvailable, 51 | // _Out_opt_ PULARGE_INTEGER lpTotalNumberOfBytes, 52 | // _Out_opt_ PULARGE_INTEGER lpTotalNumberOfFreeBytes 53 | // ); 54 | _, _, _ = GetDiskFreeSpaceEx.Call(uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(path))), 55 | uintptr(unsafe.Pointer(&lpFreeBytesAvailable)), 56 | uintptr(unsafe.Pointer(&lpTotalNumberOfBytes)), 57 | uintptr(unsafe.Pointer(&lpTotalNumberOfFreeBytes))) 58 | 59 | if uint64(lpTotalNumberOfFreeBytes) > uint64(lpTotalNumberOfBytes) { 60 | return info, fmt.Errorf("detected free space (%d) > total drive space (%d), fs corruption at (%s). please run 'fsck'", 61 | uint64(lpTotalNumberOfFreeBytes), uint64(lpTotalNumberOfBytes), path) 62 | } 63 | 64 | info = Info{ 65 | Total: uint64(lpTotalNumberOfBytes), 66 | Free: uint64(lpTotalNumberOfFreeBytes), 67 | Used: uint64(lpTotalNumberOfBytes) - uint64(lpTotalNumberOfFreeBytes), 68 | FSType: getFSType(path), 69 | } 70 | 71 | // Return values of GetDiskFreeSpace() 72 | lpSectorsPerCluster := uint32(0) 73 | lpBytesPerSector := uint32(0) 74 | lpNumberOfFreeClusters := uint32(0) 75 | lpTotalNumberOfClusters := uint32(0) 76 | 77 | // Extract values safely 78 | // BOOL WINAPI GetDiskFreeSpace( 79 | // _In_ LPCTSTR lpRootPathName, 80 | // _Out_ LPDWORD lpSectorsPerCluster, 81 | // _Out_ LPDWORD lpBytesPerSector, 82 | // _Out_ LPDWORD lpNumberOfFreeClusters, 83 | // _Out_ LPDWORD lpTotalNumberOfClusters 84 | // ); 85 | _, _, _ = GetDiskFreeSpace.Call(uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(path))), 86 | uintptr(unsafe.Pointer(&lpSectorsPerCluster)), 87 | uintptr(unsafe.Pointer(&lpBytesPerSector)), 88 | uintptr(unsafe.Pointer(&lpNumberOfFreeClusters)), 89 | uintptr(unsafe.Pointer(&lpTotalNumberOfClusters))) 90 | 91 | info.Files = uint64(lpTotalNumberOfClusters) 92 | info.Ffree = uint64(lpNumberOfFreeClusters) 93 | 94 | return info, nil 95 | } 96 | 97 | // getFSType returns the filesystem type of the underlying mounted filesystem 98 | func getFSType(path string) string { 99 | volumeNameSize, nFileSystemNameSize := uint32(260), uint32(260) 100 | var lpVolumeSerialNumber uint32 101 | var lpFileSystemFlags, lpMaximumComponentLength uint32 102 | var lpFileSystemNameBuffer, volumeName [260]uint16 103 | ps := syscall.StringToUTF16Ptr(filepath.VolumeName(path)) 104 | 105 | // Extract values safely 106 | // BOOL WINAPI GetVolumeInformation( 107 | // _In_opt_ LPCTSTR lpRootPathName, 108 | // _Out_opt_ LPTSTR lpVolumeNameBuffer, 109 | // _In_ DWORD nVolumeNameSize, 110 | // _Out_opt_ LPDWORD lpVolumeSerialNumber, 111 | // _Out_opt_ LPDWORD lpMaximumComponentLength, 112 | // _Out_opt_ LPDWORD lpFileSystemFlags, 113 | // _Out_opt_ LPTSTR lpFileSystemNameBuffer, 114 | // _In_ DWORD nFileSystemNameSize 115 | // ); 116 | 117 | _, _, _ = GetVolumeInformation.Call(uintptr(unsafe.Pointer(ps)), 118 | uintptr(unsafe.Pointer(&volumeName)), 119 | uintptr(volumeNameSize), 120 | uintptr(unsafe.Pointer(&lpVolumeSerialNumber)), 121 | uintptr(unsafe.Pointer(&lpMaximumComponentLength)), 122 | uintptr(unsafe.Pointer(&lpFileSystemFlags)), 123 | uintptr(unsafe.Pointer(&lpFileSystemNameBuffer)), 124 | uintptr(nFileSystemNameSize)) 125 | 126 | return syscall.UTF16ToString(lpFileSystemNameBuffer[:]) 127 | } -------------------------------------------------------------------------------- /internal/socks_dns_client.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "context" 5 | "net/url" 6 | "net" 7 | "errors" 8 | "golang.org/x/net/proxy" 9 | "github.com/miekg/dns" 10 | 11 | ) 12 | 13 | type SocksClient struct { 14 | Client *dns.Client 15 | } 16 | 17 | // Exchange performs a synchronous query. It sends the message m to the address 18 | // contained in a and waits for a reply. Basic use pattern with a *dns.Client: 19 | // 20 | // c := new(dns.Client) 21 | // in, rtt, err := c.Exchange(message, "127.0.0.1:53") 22 | // 23 | // Exchange does not retry a failed query, nor will it fall back to TCP in 24 | // case of truncation. 25 | // It is up to the caller to create a message that allows for larger responses to be 26 | // returned. Specifically this means adding an EDNS0 OPT RR that will advertise a larger 27 | // buffer, see SetEdns0. Messages without an OPT RR will fallback to the historic limit 28 | // of 512 bytes 29 | // To specify a local address or a timeout, the caller has to set the `Client.Dialer` 30 | // attribute appropriately 31 | func (c *SocksClient) Exchange(m *dns.Msg, proxyUri *url.URL, address string) (*dns.Msg, error) { 32 | if proxyUri == nil { 33 | return dns.Exchange(m, address); 34 | } 35 | 36 | c.Client = new(dns.Client) 37 | co, err := c.Dial(proxyUri, address) 38 | if err != nil { 39 | return nil, err 40 | } 41 | defer co.Close() 42 | r, _, err := c.Client.ExchangeWithConn(m, co) 43 | return r, err 44 | } 45 | 46 | // Dial connects to the address on the named network. 47 | func (c *SocksClient) Dial(proxyUri *url.URL, address string) (conn *dns.Conn, err error) { 48 | return c.DialContext(context.Background(), proxyUri, address) 49 | } 50 | 51 | // DialContext connects to the address on the named network, with a context.Context. 52 | func (c *SocksClient) DialContext(ctx context.Context, proxyUri *url.URL, address string) (conn *dns.Conn, err error) { 53 | d, err := FromURL(proxyUri, proxy.Direct) 54 | if err != nil { 55 | return nil, errors.New("Error connecting to proxy: " + err.Error()) 56 | } 57 | 58 | conn = new(dns.Conn) 59 | conn.Conn, err = d.Dial("tcp", address) 60 | if err != nil { 61 | return nil, err 62 | } 63 | conn.UDPSize = c.Client.UDPSize 64 | return conn, nil 65 | } 66 | 67 | func FromURL(u *url.URL, forward proxy.Dialer) (proxy.Dialer, error) { 68 | 69 | var auth *proxy.Auth 70 | if u.User != nil { 71 | auth = new(proxy.Auth) 72 | auth.User = u.User.Username() 73 | if p, ok := u.User.Password(); ok { 74 | auth.Password = p 75 | } 76 | } 77 | 78 | switch u.Scheme { 79 | case "socks4", "socks5", "socks5h": 80 | addr := u.Hostname() 81 | port := u.Port() 82 | if port == "" { 83 | port = "1080" 84 | } 85 | return proxy.SOCKS5("tcp", net.JoinHostPort(addr, port), auth, forward) 86 | } 87 | 88 | return nil, errors.New("proxy: unknown scheme: " + u.Scheme) 89 | } -------------------------------------------------------------------------------- /internal/tools/fs.go: -------------------------------------------------------------------------------- 1 | package tools 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | "unicode" 10 | "encoding/base64" 11 | "io/ioutil" 12 | "crypto/sha1" 13 | "encoding/hex" 14 | "errors" 15 | "net/http" 16 | "math/rand" 17 | "archive/zip" 18 | "bufio" 19 | 20 | "github.com/helviojunior/enumdns/pkg/log" 21 | "github.com/helviojunior/enumdns/internal/disk" 22 | ) 23 | 24 | func GetMimeType(s string) (string, error) { 25 | file, err := os.Open(s) 26 | 27 | if err != nil { 28 | return "", err 29 | } 30 | 31 | defer file.Close() 32 | 33 | buff := make([]byte, 512) 34 | 35 | // why 512 bytes ? see http://golang.org/pkg/net/http/#DetectContentType 36 | _, err = file.Read(buff) 37 | 38 | if err != nil { 39 | return "", err 40 | } 41 | 42 | filetype := http.DetectContentType(buff) 43 | if strings.Contains(filetype, ";") { 44 | s1 := strings.SplitN(filetype, ";", 2) 45 | if s1[0] != "" && strings.Contains(s1[0], "/") { 46 | filetype = s1[0] 47 | } 48 | } 49 | 50 | return filetype, nil 51 | } 52 | 53 | // CreateDir creates a directory if it does not exist, returning the final 54 | // normalized directory as a result. 55 | func CreateDir(dir string) (string, error) { 56 | var err error 57 | 58 | if strings.HasPrefix(dir, "~") { 59 | homeDir, err := os.UserHomeDir() 60 | if err != nil { 61 | return "", err 62 | } 63 | dir = filepath.Join(homeDir, dir[1:]) 64 | } 65 | 66 | dir, err = filepath.Abs(dir) 67 | if err != nil { 68 | return "", err 69 | } 70 | 71 | if err := os.MkdirAll(dir, 0755); err != nil { 72 | return "", err 73 | } 74 | 75 | return dir, nil 76 | } 77 | 78 | // CreateFileWithDir creates a file, relative to a directory, returning the 79 | // final normalized path as a result. 80 | func CreateFileWithDir(destination string) (string, error) { 81 | dir := filepath.Dir(destination) 82 | file := filepath.Base(destination) 83 | 84 | if file == "." || file == "/" { 85 | return "", fmt.Errorf("destination does not appear to be a valid file path: %s", destination) 86 | } 87 | 88 | absDir, err := CreateDir(dir) 89 | if err != nil { 90 | return "", err 91 | } 92 | 93 | absPath := filepath.Join(absDir, file) 94 | fileHandle, err := os.Create(absPath) 95 | if err != nil { 96 | return "", err 97 | } 98 | defer fileHandle.Close() 99 | 100 | return absPath, nil 101 | } 102 | 103 | func CreateDirFromFilename(destination string, s string) (string, error) { 104 | fn := SafeFileName(strings.TrimSuffix(filepath.Base(s), filepath.Ext(s))) 105 | if fn == "" { 106 | fn = "temp" 107 | } 108 | 109 | return CreateDir(filepath.Join(destination, fn)) 110 | } 111 | 112 | // SafeFileName takes a string and returns a string safe to use as 113 | // a file name. 114 | func SafeFileName(s string) string { 115 | var builder strings.Builder 116 | 117 | for _, r := range s { 118 | if unicode.IsLetter(r) || unicode.IsDigit(r) || r == '.' { 119 | builder.WriteRune(r) 120 | } else { 121 | builder.WriteRune('-') 122 | } 123 | } 124 | 125 | return builder.String() 126 | } 127 | 128 | func TempFileName(base_path, prefix, suffix string) string { 129 | randBytes := make([]byte, 16) 130 | rand.Read(randBytes) 131 | 132 | if base_path == "" { 133 | base_path = os.TempDir() 134 | 135 | di, err := disk.GetInfo(base_path, false) 136 | if err != nil { 137 | log.Debug("Error getting disk stats", "path", base_path, "err", err) 138 | } 139 | if err == nil { 140 | log.Debug("Free disk space", "path", base_path, "free", di.Free) 141 | if di.Free <= (5 * 1024 * 1024 * 1024) { // Less than 5GB 142 | currentPath, err := os.Getwd() 143 | if err != nil { 144 | log.Debug("Error getting working directory", "err", err) 145 | } 146 | if err == nil { 147 | base_path = currentPath 148 | } 149 | log.Debug("Free disk <= 5Gb, changing temp path location", "temp_path", base_path) 150 | } 151 | } 152 | } 153 | return filepath.Join(base_path, prefix+hex.EncodeToString(randBytes)+suffix) 154 | } 155 | 156 | // FileExists returns true if a path exists 157 | func FileExists(path string) bool { 158 | _, err := os.Stat(path) 159 | 160 | return !os.IsNotExist(err) 161 | } 162 | 163 | // MoveFile moves a file from a to b 164 | func MoveFile(sourcePath, destPath string) error { 165 | if err := os.Rename(sourcePath, destPath); err == nil { 166 | return nil 167 | } 168 | 169 | sourceFile, err := os.Open(sourcePath) 170 | if err != nil { 171 | return err 172 | } 173 | defer sourceFile.Close() 174 | 175 | destFile, err := os.Create(destPath) 176 | if err != nil { 177 | return err 178 | } 179 | defer destFile.Close() 180 | 181 | _, err = io.Copy(destFile, sourceFile) 182 | if err != nil { 183 | return err 184 | } 185 | 186 | err = os.Remove(sourcePath) 187 | if err != nil { 188 | return err 189 | } 190 | 191 | return nil 192 | } 193 | 194 | func EncodeFileToBase64(filename string) (string, error) { 195 | 196 | var file *os.File 197 | var err error 198 | 199 | file, err = os.Open(filename) 200 | if err != nil { 201 | return "", err 202 | } 203 | defer file.Close() 204 | 205 | // Lê o conteúdo do arquivo 206 | data, err := ioutil.ReadAll(file) 207 | if err != nil { 208 | return "", err 209 | } 210 | 211 | // Codifica em Base64 212 | encoded := base64.StdEncoding.EncodeToString(data) 213 | return encoded, nil 214 | 215 | } 216 | 217 | func GetHash(data []byte) string { 218 | h := sha1.New() 219 | h.Write(data) 220 | return hex.EncodeToString(h.Sum(nil)) 221 | } 222 | 223 | 224 | func RemoveFolder(path string) error { 225 | if path == "" { 226 | return nil 227 | } 228 | 229 | fi, err := os.Stat(path) 230 | 231 | if err != nil { 232 | return err 233 | } 234 | 235 | if fi.Mode().IsDir() { 236 | err = os.RemoveAll(path) 237 | if err != nil { 238 | return err 239 | } 240 | 241 | }else{ 242 | return errors.New("Path is not a Directory!") 243 | } 244 | 245 | return nil 246 | } 247 | 248 | func Unzip(src, dest string) error { 249 | r, err := zip.OpenReader(src) 250 | if err != nil { 251 | return err 252 | } 253 | defer r.Close() 254 | 255 | for _, f := range r.File { 256 | rc, err := f.Open() 257 | if err != nil { 258 | return err 259 | } 260 | defer rc.Close() 261 | 262 | fpath := filepath.Join(dest, f.Name) 263 | if f.FileInfo().IsDir() { 264 | os.MkdirAll(fpath, f.Mode()) 265 | } else { 266 | var fdir string 267 | if lastIndex := strings.LastIndex(fpath,string(os.PathSeparator)); lastIndex > -1 { 268 | fdir = fpath[:lastIndex] 269 | } 270 | 271 | err = os.MkdirAll(fdir, f.Mode()) 272 | if err != nil { 273 | return err 274 | } 275 | f, err := os.OpenFile( 276 | fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) 277 | if err != nil { 278 | return err 279 | } 280 | defer f.Close() 281 | 282 | _, err = io.Copy(f, rc) 283 | if err != nil { 284 | return err 285 | } 286 | } 287 | } 288 | return nil 289 | } 290 | 291 | func HasBOM(fileName string) bool { 292 | f, err := os.Open(fileName) 293 | if err != nil { 294 | return false 295 | } 296 | defer f.Close() 297 | 298 | br := bufio.NewReader(f) 299 | r, _, err := br.ReadRune() 300 | if err != nil { 301 | return false 302 | } 303 | if r != '\uFEFF' { 304 | //br.UnreadRune() // Not a BOM -- put the rune back 305 | return false 306 | } 307 | 308 | return true 309 | } 310 | -------------------------------------------------------------------------------- /internal/tools/hamming.go: -------------------------------------------------------------------------------- 1 | package tools 2 | 3 | import ( 4 | "encoding/hex" 5 | "errors" 6 | "strings" 7 | ) 8 | 9 | // HammingGroup represents a hash -> group assignment used for 10 | // inmemory hammingdistance calulations. 11 | type HammingGroup struct { 12 | GroupID uint 13 | Hash []byte 14 | } 15 | 16 | // HammingDistance calculates the number of differing bits between two byte slices. 17 | func HammingDistance(hash1, hash2 []byte) (int, error) { 18 | if len(hash1) != len(hash2) { 19 | return 0, errors.New("hash lengths do not match") 20 | } 21 | 22 | distance := 0 23 | for i := 0; i < len(hash1); i++ { 24 | x := hash1[i] ^ hash2[i] 25 | for x != 0 { 26 | distance++ 27 | x &= x - 1 28 | } 29 | } 30 | 31 | return distance, nil 32 | } 33 | 34 | // ParsePerceptionHash converts a perception hash string "p:" to a byte slice. 35 | func ParsePerceptionHash(hashStr string) ([]byte, error) { 36 | if !strings.HasPrefix(hashStr, "p:") { 37 | return nil, errors.New("invalid perception hash format: missing 'p:' prefix") 38 | } 39 | 40 | hexPart := strings.TrimPrefix(hashStr, "p:") 41 | 42 | bytes, err := hex.DecodeString(hexPart) 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | return bytes, nil 48 | } 49 | -------------------------------------------------------------------------------- /internal/tools/nameserver_others.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | 3 | package tools 4 | 5 | import ( 6 | "net/netip" 7 | "os" 8 | "strings" 9 | ) 10 | 11 | func GetDNSServers() (nameservers []netip.AddrPort) { 12 | const filename = "/etc/resolv.conf" 13 | return getLocalNameservers(filename) 14 | } 15 | 16 | func getLocalNameservers(filename string) (nameservers []netip.AddrPort) { 17 | const defaultNameserverPort = 53 18 | defaultLocalNameservers := []netip.AddrPort{ 19 | //netip.AddrPortFrom(netip.AddrFrom4([4]byte{127, 0, 0, 1}), defaultNameserverPort), 20 | //netip.AddrPortFrom(netip.AddrFrom16([16]byte{0, 0, 0, 0, 0, 0, 0, 1}), defaultNameserverPort), 21 | } 22 | 23 | data, err := os.ReadFile(filename) 24 | if err != nil { 25 | return defaultLocalNameservers 26 | } 27 | 28 | lines := strings.Split(string(data), "\n") 29 | for _, line := range lines { 30 | if line == "" { 31 | continue 32 | } 33 | fields := strings.Fields(line) 34 | if len(fields) == 0 || fields[0] != "nameserver" { 35 | continue 36 | } 37 | for _, field := range fields[1:] { 38 | ip, err := netip.ParseAddr(field) 39 | if err != nil { 40 | continue 41 | } 42 | nameservers = append(nameservers, 43 | netip.AddrPortFrom(ip, defaultNameserverPort)) 44 | } 45 | } 46 | 47 | if len(nameservers) == 0 { 48 | return defaultLocalNameservers 49 | } 50 | return nameservers 51 | } 52 | -------------------------------------------------------------------------------- /internal/tools/nameserver_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | package tools 3 | 4 | import ( 5 | "errors" 6 | "fmt" 7 | "net/netip" 8 | "syscall" 9 | "unsafe" 10 | ) 11 | 12 | func GetDNSServers() (nameservers []netip.AddrPort) { 13 | const defaultDNSPort = 53 14 | defaultLocalNameservers := []netip.AddrPort{ 15 | //netip.AddrPortFrom(netip.AddrFrom4([4]byte{127, 0, 0, 1}), defaultDNSPort), 16 | //netip.AddrPortFrom(netip.AddrFrom16([16]byte{0, 0, 0, 0, 0, 0, 0, 1}), defaultDNSPort), 17 | } 18 | 19 | adapterAddresses, err := getAdapterAddresses() 20 | if err != nil { 21 | return defaultLocalNameservers 22 | } 23 | 24 | for _, adapterAddress := range adapterAddresses { 25 | const statusUp = 0x01 26 | if adapterAddress.operStatus != statusUp { 27 | continue 28 | } 29 | 30 | if adapterAddress.firstGatewayAddress == nil { 31 | // Only search DNS servers for adapters having a gateway 32 | continue 33 | } 34 | 35 | dnsServerAddress := adapterAddress.firstDnsServerAddress 36 | for dnsServerAddress != nil { 37 | ip, ok := sockAddressToIP(dnsServerAddress.address.rawSockAddrAny) 38 | if !ok || ipIsSiteLocalAnycast(ip) { 39 | // fec0/10 IPv6 addresses are site local anycast DNS 40 | // addresses Microsoft sets by default if no other 41 | // IPv6 DNS address is set. Site local anycast is 42 | // deprecated since 2004, see 43 | // https://datatracker.ietf.org/doc/html/rfc3879 44 | dnsServerAddress = dnsServerAddress.next 45 | continue 46 | } 47 | 48 | nameserver := netip.AddrPortFrom(ip, defaultDNSPort) 49 | nameservers = append(nameservers, nameserver) 50 | dnsServerAddress = dnsServerAddress.next 51 | } 52 | } 53 | 54 | if len(nameservers) == 0 { 55 | return defaultLocalNameservers 56 | } 57 | return nameservers 58 | } 59 | 60 | var errBufferOverflowUnexpected = errors.New("unexpected buffer overflowed because buffer was large enough") 61 | 62 | func getAdapterAddresses() ( 63 | adapterAddresses []*ipAdapterAddresses, err error, 64 | ) { 65 | var buffer []byte 66 | const initialBufferLength uint32 = 15000 67 | sizeVar := initialBufferLength 68 | 69 | for { 70 | buffer = make([]byte, sizeVar) 71 | err := runProcGetAdaptersAddresses( 72 | (*ipAdapterAddresses)(unsafe.Pointer(&buffer[0])), 73 | &sizeVar) 74 | if err != nil { 75 | if err.(syscall.Errno) == syscall.ERROR_BUFFER_OVERFLOW { 76 | if sizeVar <= uint32(len(buffer)) { 77 | return nil, fmt.Errorf("%w: buffer size variable %d is "+ 78 | "equal or lower to the buffer current length %d", 79 | errBufferOverflowUnexpected, sizeVar, len(buffer)) 80 | } 81 | continue 82 | } 83 | return nil, fmt.Errorf("getting adapters addresses: %w", err) 84 | } 85 | 86 | noDataFound := sizeVar == 0 87 | if noDataFound { 88 | return nil, nil 89 | } 90 | break 91 | } 92 | 93 | adapterAddress := (*ipAdapterAddresses)(unsafe.Pointer(&buffer[0])) 94 | for adapterAddress != nil { 95 | adapterAddresses = append(adapterAddresses, adapterAddress) 96 | adapterAddress = adapterAddress.next 97 | } 98 | 99 | return adapterAddresses, nil 100 | } 101 | 102 | var procGetAdaptersAddresses = syscall.NewLazyDLL("iphlpapi.dll"). 103 | NewProc("GetAdaptersAddresses") 104 | 105 | func runProcGetAdaptersAddresses(adapterAddresses *ipAdapterAddresses, 106 | sizePointer *uint32, 107 | ) (errcode error) { 108 | const family = syscall.AF_UNSPEC 109 | const GAA_FLAG_SKIP_UNICAST = 0x0001 110 | const GAA_FLAG_SKIP_ANYCAST = 0x0002 111 | const GAA_FLAG_SKIP_MULTICAST = 0x0004 112 | const GAA_FLAG_SKIP_FRIENDLY_NAME = 0x0020 113 | const GAA_FLAG_INCLUDE_GATEWAYS = 0x0080 114 | const flags = GAA_FLAG_SKIP_UNICAST | GAA_FLAG_SKIP_ANYCAST | 115 | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_SKIP_FRIENDLY_NAME | 116 | GAA_FLAG_INCLUDE_GATEWAYS 117 | const reserved = 0 118 | // See https://learn.microsoft.com/en-us/windows/win32/api/iphlpapi/nf-iphlpapi-getadaptersaddresses 119 | r1, _, err := syscall.SyscallN(procGetAdaptersAddresses.Addr(), 120 | uintptr(family), uintptr(flags), uintptr(reserved), 121 | uintptr(unsafe.Pointer(adapterAddresses)), 122 | uintptr(unsafe.Pointer(sizePointer))) 123 | switch { 124 | case err != 0: 125 | return err 126 | case r1 != 0: 127 | return syscall.Errno(r1) 128 | default: 129 | return nil 130 | } 131 | } 132 | 133 | func sockAddressToIP(rawSockAddress *syscall.RawSockaddrAny) (ip netip.Addr, ok bool) { 134 | if rawSockAddress == nil { 135 | return netip.Addr{}, false 136 | } 137 | 138 | sockAddress, err := rawSockAddress.Sockaddr() 139 | if err != nil { 140 | return netip.Addr{}, false 141 | } 142 | 143 | switch sockAddress := sockAddress.(type) { 144 | case *syscall.SockaddrInet4: 145 | return netip.AddrFrom4([4]byte{ 146 | sockAddress.Addr[0], sockAddress.Addr[1], sockAddress.Addr[2], sockAddress.Addr[3], 147 | }), 148 | true 149 | case *syscall.SockaddrInet6: 150 | return netip.AddrFrom16([16]byte{ 151 | sockAddress.Addr[0], sockAddress.Addr[1], sockAddress.Addr[2], sockAddress.Addr[3], 152 | sockAddress.Addr[4], sockAddress.Addr[5], sockAddress.Addr[6], sockAddress.Addr[7], 153 | sockAddress.Addr[8], sockAddress.Addr[9], sockAddress.Addr[10], sockAddress.Addr[11], 154 | sockAddress.Addr[12], sockAddress.Addr[13], sockAddress.Addr[14], sockAddress.Addr[15], 155 | }), 156 | true 157 | default: 158 | return netip.Addr{}, false 159 | } 160 | } 161 | 162 | func ipIsSiteLocalAnycast(ip netip.Addr) bool { 163 | if !ip.Is6() { 164 | return false 165 | } 166 | 167 | array := ip.As16() 168 | return array[0] == 0xfe && array[1] == 0xc0 169 | } 170 | 171 | // See https://learn.microsoft.com/en-us/windows/win32/api/iptypes/ns-iptypes-ip_adapter_addresses_lh 172 | type ipAdapterAddresses struct { 173 | // The order of fields DOES matter since they are read 174 | // raw from a bytes buffer. However, we are only interested 175 | // in a few select fields, so unneeded fields are either 176 | // named as "_" or removed if they are after the fields 177 | // we are interested in. 178 | _ uint32 179 | _ uint32 180 | next *ipAdapterAddresses 181 | _ *byte 182 | _ *ipAdapterUnicastAddress 183 | _ *ipAdapterAnycastAddress 184 | _ *ipAdapterMulticastAddress 185 | firstDnsServerAddress *ipAdapterDnsServerAdapter 186 | _ *uint16 187 | _ *uint16 188 | _ *uint16 189 | _ [syscall.MAX_ADAPTER_ADDRESS_LENGTH]byte 190 | _ uint32 191 | _ uint32 192 | _ uint32 193 | _ uint32 194 | operStatus uint32 195 | _ uint32 196 | _ [16]uint32 197 | _ *ipAdapterPrefix 198 | _ uint64 199 | _ uint64 200 | _ *ipAdapterWinsServerAddress 201 | firstGatewayAddress *ipAdapterGatewayAddress 202 | // Additional fields not needed here 203 | } 204 | 205 | type ipAdapterUnicastAddress struct { 206 | // The order of fields DOES matter since they are read raw 207 | // from a bytes buffer. However, we are not interested in 208 | // the value of any field, so they are all named as "_". 209 | _ uint32 210 | _ uint32 211 | _ *ipAdapterUnicastAddress 212 | _ ipAdapterSocketAddress 213 | _ int32 214 | _ int32 215 | _ int32 216 | _ uint32 217 | _ uint32 218 | _ uint32 219 | _ uint8 220 | } 221 | 222 | type ipAdapterAnycastAddress struct { 223 | // The order of fields DOES matter since they are read raw 224 | // from a bytes buffer. However, we are not interested in 225 | // the value of any field, so they are all named as "_". 226 | _ uint32 227 | _ uint32 228 | _ *ipAdapterAnycastAddress 229 | _ ipAdapterSocketAddress 230 | } 231 | 232 | type ipAdapterMulticastAddress struct { 233 | // The order of fields DOES matter since they are read raw 234 | // from a bytes buffer. However, we are only interested in 235 | // a few select fields, so unneeded fields are named as "_". 236 | _ uint32 237 | _ uint32 238 | _ *ipAdapterMulticastAddress 239 | _ ipAdapterSocketAddress 240 | } 241 | 242 | type ipAdapterDnsServerAdapter struct { 243 | // The order of fields DOES matter since they are read raw 244 | // from a bytes buffer. However, we are only interested in 245 | // a few select fields, so unneeded fields are named as "_". 246 | _ uint32 247 | _ uint32 248 | next *ipAdapterDnsServerAdapter 249 | address ipAdapterSocketAddress 250 | } 251 | 252 | type ipAdapterPrefix struct { 253 | _ uint32 254 | _ uint32 255 | _ *ipAdapterPrefix 256 | _ ipAdapterSocketAddress 257 | _ uint32 258 | } 259 | 260 | type ipAdapterWinsServerAddress struct { 261 | _ uint32 262 | _ uint32 263 | _ *ipAdapterWinsServerAddress 264 | _ ipAdapterSocketAddress 265 | } 266 | 267 | type ipAdapterGatewayAddress struct { 268 | _ uint32 269 | _ uint32 270 | _ *ipAdapterGatewayAddress 271 | _ ipAdapterSocketAddress 272 | } 273 | 274 | type ipAdapterSocketAddress struct { 275 | rawSockAddrAny *syscall.RawSockaddrAny 276 | } -------------------------------------------------------------------------------- /internal/tools/net.go: -------------------------------------------------------------------------------- 1 | package tools 2 | 3 | import ( 4 | "errors" 5 | "encoding/binary" 6 | "net" 7 | "strings" 8 | "net/url" 9 | //"context" 10 | "github.com/miekg/dns" 11 | 12 | "github.com/helviojunior/enumdns/internal" 13 | ) 14 | 15 | var privateNets = []string{ 16 | "192.168.0.0/16", 17 | "10.0.0.0/8", 18 | "172.16.0.0/12", 19 | "127.0.0.0/8", 20 | } 21 | 22 | // IpsInCIDR returns a list of usable IP addresses in a given CIDR block 23 | // excluding network and broadcast addresses for CIDRs larger than /31. 24 | func IpsInCIDR(cidr string) ([]string, error) { 25 | _, ipnet, err := net.ParseCIDR(cidr) 26 | if err != nil { 27 | return nil, err 28 | } 29 | 30 | mask := binary.BigEndian.Uint32(ipnet.Mask) 31 | start := binary.BigEndian.Uint32(ipnet.IP) 32 | end := (start & mask) | (mask ^ 0xFFFFFFFF) 33 | 34 | var ips []string 35 | ip := make(net.IP, 4) // Preallocate buffer 36 | 37 | // Iterate over the range of IPs 38 | for i := start; i <= end; i++ { 39 | // Exclude network and broadcast addresses in larger CIDR ranges 40 | if !(i&0xFF == 255 || i&0xFF == 0) || ipnet.Mask[3] >= 30 { 41 | binary.BigEndian.PutUint32(ip, i) 42 | ips = append(ips, ip.String()) 43 | } 44 | } 45 | 46 | return ips, nil 47 | } 48 | 49 | func GetValidDnsSuffix(dnsServer string, suffix string, proxyUri *url.URL) (string, error) { 50 | suffix = strings.Trim(suffix, ". ") 51 | if suffix == "" { 52 | return "", errors.New("empty suffix string") 53 | } 54 | 55 | suffix = strings.ToLower(suffix) + "." 56 | i := false 57 | 58 | m := new(dns.Msg) 59 | m.Id = dns.Id() 60 | m.RecursionDesired = true 61 | 62 | m.Question = make([]dns.Question, 1) 63 | m.Question[0] = dns.Question{suffix, dns.TypeSOA, dns.ClassINET} 64 | 65 | c := new(internal.SocksClient) 66 | in, err := c.Exchange(m, proxyUri, dnsServer); 67 | if err != nil { 68 | return "", err 69 | }else{ 70 | 71 | for _, ans1 := range in.Answer { 72 | if _, ok := ans1.(*dns.SOA); ok { 73 | i = true 74 | } 75 | } 76 | 77 | } 78 | 79 | if i == false { 80 | return "", errors.New("SOA not found for domain '"+ suffix + "'") 81 | } 82 | 83 | return suffix, nil 84 | 85 | } 86 | 87 | func IsPrivateIP(ipAddr string) bool { 88 | ip := net.ParseIP(ipAddr) 89 | for _, netip := range privateNets { 90 | _, subnet, _ := net.ParseCIDR(netip) 91 | if subnet.Contains(ip) { 92 | return true 93 | } 94 | } 95 | 96 | return false 97 | } 98 | 99 | 100 | func GetDefaultDnsServer(fallback string) string { 101 | if fallback == "" { 102 | fallback = "8.8.8.8" 103 | } 104 | 105 | srv := GetDNSServers() 106 | if len(srv) == 0 { 107 | return fallback 108 | } 109 | 110 | return srv[0].Addr().String() 111 | } 112 | -------------------------------------------------------------------------------- /internal/tools/slices.go: -------------------------------------------------------------------------------- 1 | package tools 2 | 3 | import ( 4 | "time" 5 | 6 | "math/rand" 7 | ) 8 | 9 | // SliceHasStr checks if a slice has a string 10 | func SliceHasStr(slice []string, item string) bool { 11 | for _, s := range slice { 12 | if s == item { 13 | return true 14 | } 15 | } 16 | 17 | return false 18 | } 19 | 20 | // SliceHasInt checks if a slice has an int 21 | func SliceHasInt(slice []int, item int) bool { 22 | for _, s := range slice { 23 | if s == item { 24 | return true 25 | } 26 | } 27 | 28 | return false 29 | } 30 | 31 | 32 | // SliceHasInt checks if a slice has an int 33 | func SliceHasUInt16(slice []uint16, item uint16) bool { 34 | for _, s := range slice { 35 | if s == item { 36 | return true 37 | } 38 | } 39 | 40 | return false 41 | } 42 | 43 | 44 | // UniqueIntSlice returns a slice of unique ints 45 | func UniqueIntSlice(slice []int) []int { 46 | seen := make(map[int]bool) 47 | result := []int{} 48 | 49 | for _, num := range slice { 50 | if !seen[num] { 51 | seen[num] = true 52 | result = append(result, num) 53 | } 54 | } 55 | 56 | return result 57 | } 58 | 59 | // ShuffleStr shuffles a slice of strings 60 | func ShuffleStr(slice []string) { 61 | source := rand.NewSource(time.Now().UnixNano()) 62 | rng := rand.New(source) 63 | 64 | // Fisher-Yates shuffle algorithm 65 | for i := len(slice) - 1; i > 0; i-- { 66 | j := rng.Intn(i + 1) 67 | slice[i], slice[j] = slice[j], slice[i] 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /internal/tools/string.go: -------------------------------------------------------------------------------- 1 | package tools 2 | 3 | import ( 4 | "strconv" 5 | ) 6 | 7 | // LeftTrucate a string if its more than max 8 | func LeftTrucate(s string, max int) string { 9 | if len(s) <= max { 10 | return s 11 | } 12 | 13 | return s[max:] 14 | } 15 | 16 | func FormatInt(n int) string { 17 | return FormatInt64(int64(n)) 18 | } 19 | 20 | func FormatInt64(n int64) string { 21 | in := strconv.FormatInt(n, 10) 22 | numOfDigits := len(in) 23 | if n < 0 { 24 | numOfDigits-- // First character is the - sign (not a digit) 25 | } 26 | numOfCommas := (numOfDigits - 1) / 3 27 | 28 | out := make([]byte, len(in)+numOfCommas) 29 | if n < 0 { 30 | in, out[0] = in[1:], '-' 31 | } 32 | 33 | for i, j, k := len(in)-1, len(out)-1, 0; ; i, j = i-1, j-1 { 34 | out[j] = in[i] 35 | if i == 0 { 36 | return string(out) 37 | } 38 | if k++; k == 3 { 39 | j, k = j-1, 0 40 | out[j] = '.' 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /internal/tools/time.go: -------------------------------------------------------------------------------- 1 | package tools 2 | 3 | import ( 4 | "time" 5 | "math/rand" 6 | ) 7 | 8 | // Float64ToTime takes a float64 as number of seconds since unix epoch and returns time.Time 9 | // 10 | // example field where this is used (expires field): 11 | // 12 | // https://chromedevtools.github.io/devtools-protocol/tot/Network/#type-Cookie 13 | func Float64ToTime(f float64) time.Time { 14 | if f == 0 { 15 | // Return zero value for session cookies 16 | return time.Time{} 17 | } 18 | return time.Unix(0, int64(f*float64(time.Second))) 19 | } 20 | 21 | func RandSleep() { 22 | rand.Seed(time.Now().UnixNano()) 23 | n := 4 + rand.Intn(6) //4 to 10 24 | time.Sleep(time.Second/time.Duration(n)) 25 | } -------------------------------------------------------------------------------- /internal/version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | var ( 4 | Version = "dev" 5 | 6 | GitHash = "dev" 7 | GoBuildEnv = "dev" 8 | GoBuildTime = "dev" 9 | ) 10 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/helviojunior/enumdns/cmd" 4 | 5 | func main() { 6 | cmd.Execute() 7 | } 8 | -------------------------------------------------------------------------------- /pkg/database/db.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net/url" 7 | "os" 8 | "path/filepath" 9 | "time" 10 | 11 | "github.com/glebarez/sqlite" 12 | "github.com/helviojunior/enumdns/pkg/models" 13 | "gorm.io/driver/mysql" 14 | "gorm.io/driver/postgres" 15 | "gorm.io/gorm" 16 | "gorm.io/gorm/logger" 17 | ) 18 | 19 | // Connection returns a Database connection based on a URI 20 | func Connection(uri string, shouldExist, debug bool) (*gorm.DB, error) { 21 | var err error 22 | var c *gorm.DB 23 | 24 | db, err := url.Parse(uri) 25 | if err != nil { 26 | return nil, err 27 | } 28 | 29 | var config = &gorm.Config{} 30 | if debug { 31 | config.Logger = logger.Default.LogMode(logger.Info) 32 | } else { 33 | config.Logger = logger.Default.LogMode(logger.Error) 34 | } 35 | 36 | switch db.Scheme { 37 | case "sqlite": 38 | if shouldExist { 39 | dbpath := filepath.Join(db.Host, db.Path) 40 | dbpath = filepath.Clean(dbpath) 41 | 42 | if _, err := os.Stat(dbpath); os.IsNotExist(err) { 43 | return nil, fmt.Errorf("sqlite database file does not exist: %s", dbpath) 44 | } else if err != nil { 45 | return nil, fmt.Errorf("error checking sqlite database file: %w", err) 46 | } 47 | } 48 | 49 | //config.SkipDefaultTransaction = true 50 | 51 | c, err = gorm.Open(sqlite.Open(db.Host+db.Path+"?cache=shared"), config) 52 | if err != nil { 53 | return nil, err 54 | } 55 | c.Exec("PRAGMA foreign_keys = ON") 56 | c.Exec("PRAGMA cache_size = 10000") 57 | case "postgres": 58 | c, err = gorm.Open(postgres.Open(uri), config) 59 | if err != nil { 60 | return nil, err 61 | } 62 | case "mysql": 63 | c, err = gorm.Open(mysql.Open(uri), config) 64 | if err != nil { 65 | return nil, err 66 | } 67 | default: 68 | return nil, errors.New("invalid db uri scheme") 69 | } 70 | 71 | // run database migrations on the connection 72 | if err := c.AutoMigrate( 73 | &Application{}, 74 | &models.Result{}, 75 | &models.FQDNData{}, 76 | ); err != nil { 77 | return nil, err 78 | } 79 | 80 | //Check if app name was inserted at application info table 81 | var count int64 82 | if err := c.Model(&Application{}).Count(&count).Error; err != nil { 83 | return nil, err 84 | } 85 | 86 | if count == 0 { 87 | defaultApp := Application{ 88 | Application: "enumdns", 89 | CreatedAt: time.Now(), 90 | } 91 | if err := c.Create(&defaultApp).Error; err != nil { 92 | return nil, err 93 | } 94 | } 95 | 96 | return c, nil 97 | } 98 | 99 | type Application struct { 100 | Application string `json:"application"` 101 | CreatedAt time.Time `json:"created_at"` 102 | } 103 | 104 | func (Application) TableName() string { 105 | return "application_info" 106 | } 107 | -------------------------------------------------------------------------------- /pkg/log/log.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "os" 5 | "fmt" 6 | 7 | "github.com/charmbracelet/lipgloss" 8 | "github.com/charmbracelet/log" 9 | ) 10 | 11 | // LLogger is a charmbracelet logger type redefinition 12 | type LLogger = log.Logger 13 | 14 | // Logger is this package level logger 15 | var Logger *LLogger 16 | 17 | func init() { 18 | styles := log.DefaultStyles() 19 | styles.Keys["err"] = lipgloss.NewStyle().Foreground(lipgloss.Color("204")) 20 | styles.Values["err"] = lipgloss.NewStyle().Bold(true) 21 | 22 | Logger = log.NewWithOptions(os.Stderr, log.Options{ 23 | ReportTimestamp: false, 24 | }) 25 | Logger.SetStyles(styles) 26 | Logger.SetLevel(log.InfoLevel) 27 | } 28 | 29 | // EnableDebug enabled debug logging and caller reporting 30 | func EnableDebug() { 31 | Logger.SetLevel(log.DebugLevel) 32 | Logger.SetReportCaller(true) 33 | } 34 | 35 | // EnableSilence will silence most logs, except this written with Print 36 | func EnableSilence() { 37 | Logger.SetLevel(log.FatalLevel + 100) 38 | } 39 | 40 | // Debug logs debug messages 41 | func Debug(msg string, keyvals ...interface{}) { 42 | Logger.Helper() 43 | Logger.Debug(msg, keyvals...) 44 | } 45 | func Debugf(format string, a ...interface{}) { 46 | Logger.Helper() 47 | Logger.Debug(fmt.Sprintf(format, a...) ) 48 | } 49 | 50 | // Info logs info messages 51 | func Info(msg string, keyvals ...interface{}) { 52 | Logger.Helper() 53 | Logger.Info(msg, keyvals...) 54 | } 55 | func Infof(format string, a ...interface{}) { 56 | Logger.Helper() 57 | Logger.Info(fmt.Sprintf(format, a...) ) 58 | } 59 | 60 | 61 | // Warn logs warning messages 62 | func Warn(msg string, keyvals ...interface{}) { 63 | Logger.Helper() 64 | Logger.Warn(msg, keyvals...) 65 | } 66 | func Warnf(format string, a ...interface{}) { 67 | Logger.Helper() 68 | Logger.Warn(fmt.Sprintf(format, a...) ) 69 | } 70 | 71 | 72 | // Error logs error messages 73 | func Error(msg string, keyvals ...interface{}) { 74 | Logger.Helper() 75 | Logger.Error(msg, keyvals...) 76 | } 77 | func Errorf(format string, a ...interface{}) { 78 | Logger.Helper() 79 | Logger.Error(fmt.Sprintf(format, a...) ) 80 | } 81 | 82 | // Fatal logs fatal messages and panics 83 | func Fatal(msg string, keyvals ...interface{}) { 84 | Logger.Helper() 85 | Logger.Fatal(msg, keyvals...) 86 | } 87 | func Fatalf(format string, a ...interface{}) { 88 | Logger.Helper() 89 | Logger.Fatal(fmt.Sprintf(format, a...) ) 90 | } 91 | 92 | 93 | // Print logs messages regardless of level 94 | func Print(msg string, keyvals ...interface{}) { 95 | Logger.Helper() 96 | Logger.Print(msg, keyvals...) 97 | } 98 | func Printf(format string, a ...interface{}) { 99 | Logger.Helper() 100 | Logger.Print(fmt.Sprintf(format, a...) ) 101 | } 102 | 103 | // With returns a sublogger with a prefix 104 | func With(keyvals ...interface{}) *LLogger { 105 | return Logger.With(keyvals...) 106 | } -------------------------------------------------------------------------------- /pkg/models/models.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "time" 5 | "encoding/json" 6 | "strings" 7 | 8 | "fmt" 9 | "crypto/sha1" 10 | "encoding/hex" 11 | 12 | "github.com/helviojunior/enumdns/internal/tools" 13 | "gorm.io/gorm" 14 | "gorm.io/gorm/clause" 15 | ) 16 | 17 | // Result is a github.com/helviojunior/enumdnsenumdns result 18 | type Result struct { 19 | ID uint `json:"id" gorm:"primarykey"` 20 | 21 | TestId string `gorm:"column:test_id"` 22 | Hash string `gorm:"column:hash;index:,unique;"` 23 | FQDN string `gorm:"column:fqdn"` 24 | RType string `gorm:"column:result_type"` 25 | IPv4 string `gorm:"column:ipv4"` 26 | IPv6 string `gorm:"column:ipv6"` 27 | Target string `gorm:"column:target"` 28 | Ptr string `gorm:"column:ptr"` 29 | Txt string `gorm:"column:txt"` 30 | CloudProduct string `gorm:"column:cloud_product"` 31 | SaaSProduct string `gorm:"column:saas_product"` 32 | Datacenter string `gorm:"column:datacenter"` 33 | ProbedAt time.Time `gorm:"column:probed_at"` 34 | 35 | DC bool `gorm:"column:dc"` 36 | GC bool `gorm:"column:gc"` 37 | 38 | Exists bool `gorm:"column:exists"` 39 | 40 | // Failed flag set if the result should be considered failed 41 | Failed bool `gorm:"column:failed" gorm:"index:idx_exists"` 42 | FailedReason string `gorm:"column:failed_reason"` 43 | 44 | } 45 | 46 | func (*Result) TableName() string { 47 | return "results" 48 | } 49 | 50 | func (result *Result) BeforeCreate(tx *gorm.DB) (err error) { 51 | _calcHash(&result.Hash, result.String()) 52 | 53 | tx.Statement.AddClause(clause.OnConflict{ 54 | //Columns: cols, 55 | Columns: []clause.Column{{Name: "hash"}}, 56 | UpdateAll: true, 57 | }) 58 | return nil 59 | } 60 | 61 | /* Custom Marshaller for Result */ 62 | func (result Result) MarshalJSON() ([]byte, error) { 63 | return json.Marshal(&struct { 64 | FQDN string `json:"fqdn"` 65 | RType string `json:"result_type"` 66 | IPv4 string `json:"ipv4,omitempty"` 67 | IPv6 string `json:"ipv6,omitempty"` 68 | Target string `json:"target,omitempty"` 69 | Ptr string `json:"ptr,omitempty"` 70 | Txt string `json:"txt,omitempty"` 71 | CloudProduct string `json:"cloud_product,omitempty"` 72 | SaaSProduct string `json:"saas_product,omitempty"` 73 | Datacenter string `json:"datacenter,omitempty"` 74 | DC bool `json:"dc"` 75 | GC bool `json:"gc"` 76 | ProbedAt string `json:"probed_at"` 77 | 78 | }{ 79 | FQDN : strings.Trim(strings.ToLower(result.FQDN), ". "), 80 | RType : strings.ToUpper(result.RType), 81 | ProbedAt : result.ProbedAt.Format(time.RFC3339), 82 | IPv4 : result.IPv4, 83 | IPv6 : result.IPv6, 84 | Target : strings.Trim(strings.ToLower(result.Target), ". "), 85 | Ptr : strings.Trim(strings.ToLower(result.Ptr), ". "), 86 | Txt : result.Txt, 87 | DC : result.DC, 88 | GC : result.GC, 89 | CloudProduct : result.CloudProduct, 90 | SaaSProduct : result.SaaSProduct, 91 | Datacenter : result.Datacenter, 92 | }) 93 | } 94 | 95 | func (result Result) Clone() *Result { 96 | return &Result{ 97 | TestId : result.TestId, 98 | FQDN : result.FQDN, 99 | RType : result.RType, 100 | IPv4 : result.IPv4, 101 | IPv6 : result.IPv6, 102 | Target : result.Target, 103 | Ptr : result.Ptr, 104 | Txt : result.Txt, 105 | DC : result.DC, 106 | GC : result.GC, 107 | CloudProduct : result.CloudProduct, 108 | SaaSProduct : result.SaaSProduct, 109 | Datacenter : result.Datacenter, 110 | ProbedAt : result.ProbedAt, 111 | Exists : result.Exists, 112 | Failed : result.Failed, 113 | FailedReason : result.FailedReason, 114 | } 115 | } 116 | 117 | type FQDNData struct { 118 | ID uint `json:"id" gorm:"primarykey"` 119 | 120 | Hash string `gorm:"column:hash;index:,unique;"` 121 | FQDN string `gorm:"column:fqdn"` 122 | Source string `gorm:"column:source"` 123 | ProbedAt time.Time `gorm:"column:probed_at"` 124 | } 125 | 126 | func (*FQDNData) TableName() string { 127 | return "fqdn_results" 128 | } 129 | 130 | func (fqdn *FQDNData) BeforeCreate(tx *gorm.DB) (err error) { 131 | _calcHash(&fqdn.Hash, fqdn.FQDN) 132 | 133 | tx.Statement.AddClause(clause.OnConflict{ 134 | //Columns: cols, 135 | Columns: []clause.Column{{Name: "hash"}}, 136 | DoNothing: true, 137 | }) 138 | return nil 139 | } 140 | 141 | func (result Result) ToFqdn() *FQDNData { 142 | 143 | if !result.Exists { 144 | return nil 145 | } 146 | 147 | return &FQDNData{ 148 | FQDN : strings.Trim(strings.ToLower(result.FQDN), ". "), 149 | Source : "Enum", 150 | ProbedAt : result.ProbedAt, 151 | } 152 | } 153 | 154 | func (result Result) Equal(r1 Result) bool { 155 | if result.RType != r1.RType { 156 | return false 157 | } 158 | if result.FQDN != r1.FQDN { 159 | return false 160 | } 161 | switch result.RType { 162 | case "A": 163 | return result.IPv4 == r1.IPv4 164 | case "AAAA": 165 | return result.IPv6 == r1.IPv6 166 | case "CNAME", "SRV", "NS", "SOA": 167 | return strings.Trim(strings.ToLower(result.Target), ". ") == strings.Trim(strings.ToLower(r1.Target), ". ") 168 | case "PTR": 169 | r2 := strings.Trim(strings.ToLower(result.Ptr), ". ") == strings.Trim(strings.ToLower(r1.Ptr), ". ") 170 | if result.IPv6 != "" { 171 | return result.IPv6 == r1.IPv6 && r2 172 | }else{ 173 | return result.IPv4 == r1.IPv4 && r2 174 | } 175 | default: 176 | if result.IPv6 != "" { 177 | return result.IPv6 == r1.IPv6 178 | }else if result.IPv4 != "" { 179 | return result.IPv4 == r1.IPv4 180 | }else if result.Target != "" { 181 | return strings.Trim(strings.ToLower(result.Target), ". ") == strings.Trim(strings.ToLower(r1.Target), ". ") 182 | }else if result.Ptr != "" { 183 | return strings.Trim(strings.ToLower(result.Ptr), ". ") == strings.Trim(strings.ToLower(r1.Ptr), ". ") 184 | } 185 | } 186 | 187 | return false 188 | } 189 | 190 | func (result Result) String() string { 191 | r := strings.Trim(strings.ToLower(result.FQDN), ". ") + ": " 192 | switch result.RType { 193 | case "A": 194 | r += result.IPv4 195 | case "AAAA": 196 | r += result.IPv6 197 | case "CNAME", "SRV", "NS", "SOA", "MX": 198 | r += strings.Trim(strings.ToLower(result.Target), ". ") 199 | case "PTR": 200 | r += strings.Trim(strings.ToLower(result.Ptr), ". ") + " -> " 201 | if result.IPv6 != "" { 202 | r += result.IPv6 203 | }else{ 204 | r += result.IPv4 205 | } 206 | case "TXT": 207 | r += result.Txt 208 | default: 209 | r = r + result.RType + " " 210 | if result.IPv6 != "" { 211 | r += result.IPv6 212 | }else if result.IPv4 != "" { 213 | r += result.IPv4 214 | }else if result.Target != "" { 215 | r += strings.Trim(strings.ToLower(result.Target), ". ") 216 | }else if result.Ptr != "" { 217 | r += result.Ptr 218 | } 219 | } 220 | if result.CloudProduct != "" || result.SaaSProduct != "" || result.Datacenter != "" { 221 | prod := "" 222 | 223 | if result.CloudProduct != "" { 224 | prod += "Cloud = " + result.CloudProduct 225 | } 226 | if result.SaaSProduct != "" { 227 | if prod != "" { 228 | prod += ", " 229 | } 230 | prod += "SaaS = " + result.SaaSProduct 231 | } 232 | if result.Datacenter != "" { 233 | if prod != "" { 234 | prod += ", " 235 | } 236 | prod += "Datacenter = " + result.Datacenter 237 | } 238 | 239 | r += " (" + prod + ")" 240 | } 241 | if result.DC || result.GC { 242 | ad := []string{} 243 | if result.GC { 244 | ad = append(ad, "GC") 245 | } 246 | if result.DC { 247 | ad = append(ad, "DC") 248 | } 249 | r += " (" + strings.Join(ad, ", ") + ")" 250 | } 251 | return r 252 | } 253 | 254 | func (result Result) GetHash() string { 255 | b_data := []byte(result.String()) 256 | return tools.GetHash(b_data) 257 | } 258 | 259 | func (result Result) GetCompHash() string { 260 | r := "" 261 | switch result.RType { 262 | case "SOA": 263 | r += "000" 264 | case "SRV": 265 | r += "010" 266 | case "NS": 267 | r += "020" 268 | case "CNAME": 269 | r += "030" 270 | case "A": 271 | r += "040" 272 | case "AAAA": 273 | r += "050" 274 | case "PTR": 275 | r += "060" 276 | default: 277 | if !result.Exists { 278 | r += "990" 279 | }else{ 280 | r += "900" 281 | } 282 | } 283 | 284 | r += result.String() 285 | return r 286 | } 287 | 288 | func SliceHasResult(s []*Result, r *Result) bool { 289 | for _, a := range s { 290 | if r.Equal(*a) { 291 | return true 292 | } 293 | /* 294 | if a.FQDN != r.FQDN || a.RType != r.RType || a.Ptr != r.Ptr { 295 | continue 296 | } 297 | switch a.RType { 298 | case "A": 299 | if a.IPv4 == r.IPv4 { 300 | return true 301 | } 302 | case "AAAA": 303 | if a.IPv6 == r.IPv6 { 304 | return true 305 | } 306 | case "CNAME": 307 | if a.Target == r.Target { 308 | return true 309 | } 310 | }*/ 311 | } 312 | return false 313 | } 314 | 315 | 316 | func _calcHash(outValue *string, keyvals ...interface{}) { 317 | 318 | data := "" 319 | for _, v := range keyvals { 320 | if _, ok := v.(int); ok { 321 | data += fmt.Sprintf("%d,", v) 322 | }else{ 323 | data += fmt.Sprintf("%s,", v) 324 | } 325 | } 326 | 327 | h := sha1.New() 328 | h.Write([]byte(data)) 329 | 330 | *outValue = hex.EncodeToString(h.Sum(nil)) 331 | 332 | } -------------------------------------------------------------------------------- /pkg/readers/crtsh.go: -------------------------------------------------------------------------------- 1 | package readers 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | "errors" 7 | "strings" 8 | 9 | "io" 10 | "net/http" 11 | "regexp" 12 | "time" 13 | 14 | "github.com/helviojunior/enumdns/internal/tools" 15 | "github.com/helviojunior/enumdns/pkg/log" 16 | ) 17 | 18 | type CrtShReader struct { 19 | Options *CrtShReaderOptions 20 | } 21 | 22 | type CrtShReaderOptions struct { 23 | Timeout time.Duration 24 | ProxyUri *url.URL 25 | } 26 | 27 | func NewCrtShReader(opts *CrtShReaderOptions) *CrtShReader { 28 | if opts.Timeout <= (60 * time.Second) { 29 | opts.Timeout = (60 * time.Second) 30 | } 31 | return &CrtShReader{ 32 | Options: opts, 33 | } 34 | } 35 | 36 | // Read from a https://crt.sh. 37 | func (crtr *CrtShReader) ReadFromCrtsh(domain string, outList *[]string, fqdnList *[]string) error { 38 | domain = strings.Trim(strings.ToLower(domain), ".") 39 | crtUrl := fmt.Sprintf("https://crt.sh/?CN=%s", domain) 40 | 41 | client := &http.Client{ 42 | Timeout: crtr.Options.Timeout, 43 | } 44 | 45 | if crtr.Options.ProxyUri != nil { 46 | // Create transport with proxy 47 | transport := &http.Transport{ 48 | Proxy: http.ProxyURL(crtr.Options.ProxyUri), 49 | } 50 | 51 | // Create HTTP client with timeout and transport 52 | client = &http.Client{ 53 | Timeout: crtr.Options.Timeout, 54 | Transport: transport, 55 | } 56 | } 57 | 58 | resp, err := crtr.fetchWithRetry(client, crtUrl) // client.Get(crtUrl) 59 | if err != nil { 60 | return err 61 | } 62 | defer resp.Body.Close() 63 | 64 | body, err := io.ReadAll(resp.Body) 65 | if err != nil { 66 | return err 67 | } 68 | 69 | *outList = append(*outList, domain) 70 | 71 | // Extract all ... content 72 | re := regexp.MustCompile(`(?i)(.*?)`) 73 | matches := re.FindAllStringSubmatch(string(body), -1) 74 | 75 | for _, match := range matches { 76 | candidate := strings.ToLower(strings.TrimSpace(strings.Replace(match[1], "*.", "", -1))) 77 | if candidate != "" && !strings.Contains(candidate, "white-space:normal") { 78 | // Check if it is a valid FQDN 79 | _, err := url.Parse(fmt.Sprintf("https://%s/", candidate)) 80 | if err != nil { 81 | log.Debug("Invalid host", "host", candidate, "err", err) 82 | }else{ 83 | candidate = strings.Trim(candidate, ".") 84 | candidate = strings.Replace(candidate, fmt.Sprintf(".%s", domain), "", -1) 85 | candidate = strings.Replace(candidate, domain, "", -1) 86 | if candidate != "" { 87 | if !tools.SliceHasStr(*outList, candidate) { 88 | log.Debug("Match", "domain", domain, "host", candidate) 89 | *outList = append(*outList, candidate) 90 | } 91 | fqdn := fmt.Sprintf("%s.%s", candidate, domain) 92 | if !tools.SliceHasStr(*fqdnList, fqdn) { 93 | *fqdnList = append(*fqdnList, fqdn) 94 | } 95 | } 96 | } 97 | } 98 | } 99 | 100 | return nil 101 | } 102 | 103 | func (crtr *CrtShReader) fetchWithRetry(client *http.Client, crtUrl string) (*http.Response, error) { 104 | var resp *http.Response 105 | var err error 106 | 107 | maxRetries := 3 108 | 109 | for i := 0; i < maxRetries; i++ { 110 | resp, err = client.Get(crtUrl) 111 | if err == nil { 112 | return resp, nil 113 | } 114 | if i < maxRetries-1 { 115 | time.Sleep(time.Second * time.Duration(10 * i)) 116 | } 117 | } 118 | 119 | return nil, errors.New("failed after 3 retries: " + err.Error()) 120 | } -------------------------------------------------------------------------------- /pkg/readers/file.go: -------------------------------------------------------------------------------- 1 | package readers 2 | 3 | import ( 4 | "bufio" 5 | //"fmt" 6 | "net/url" 7 | "os" 8 | //"strconv" 9 | "strings" 10 | 11 | "github.com/helviojunior/enumdns/internal/tools" 12 | "github.com/helviojunior/enumdns/pkg/log" 13 | ) 14 | 15 | // FileReader is a reader that expects a file with targets that 16 | // is newline delimited. 17 | type FileReader struct { 18 | Options *FileReaderOptions 19 | } 20 | 21 | // FileReaderOptions are options for the file reader 22 | type FileReaderOptions struct { 23 | DnsSuffixFile string 24 | HostFile string 25 | DnsServer string 26 | IgnoreNonexistent bool 27 | ProxyUri *url.URL 28 | } 29 | 30 | // NewFileReader prepares a new file reader 31 | func NewFileReader(opts *FileReaderOptions) *FileReader { 32 | return &FileReader{ 33 | Options: opts, 34 | } 35 | } 36 | 37 | // Read from a file. 38 | func (fr *FileReader) ReadDnsList(outList *[]string) error { 39 | 40 | var file *os.File 41 | var err error 42 | 43 | file, err = os.Open(fr.Options.DnsSuffixFile) 44 | if err != nil { 45 | return err 46 | } 47 | defer file.Close() 48 | 49 | scanner := bufio.NewScanner(file) 50 | for scanner.Scan() { 51 | candidate := scanner.Text() 52 | if candidate == "" { 53 | continue 54 | } 55 | 56 | //Check if DNS exists 57 | s, err := tools.GetValidDnsSuffix(fr.Options.DnsServer, candidate, fr.Options.ProxyUri) 58 | if err != nil { 59 | if !fr.Options.IgnoreNonexistent { 60 | return err 61 | } 62 | 63 | log.Warnf("DNS suffix (%s) does not exists: %s", candidate, err.Error()) 64 | } 65 | 66 | if s == "" { 67 | continue 68 | } 69 | 70 | if !tools.SliceHasStr(*outList, s){ 71 | *outList = append(*outList, s) 72 | } 73 | 74 | } 75 | 76 | return scanner.Err() 77 | } 78 | 79 | func (fr *FileReader) ReadWordList(outList *[]string) error { 80 | return fr.readFileList(fr.Options.HostFile, outList) 81 | } 82 | 83 | // Read from a file. 84 | func (fr *FileReader) readFileList(fileName string, outList *[]string) error { 85 | 86 | var file *os.File 87 | var err error 88 | 89 | file, err = os.Open(fileName) 90 | if err != nil { 91 | return err 92 | } 93 | defer file.Close() 94 | 95 | scanner := bufio.NewScanner(file) 96 | for scanner.Scan() { 97 | candidate := scanner.Text() 98 | if candidate == "" { 99 | continue 100 | } 101 | 102 | *outList = append(*outList, strings.ToLower(candidate)) 103 | } 104 | 105 | return scanner.Err() 106 | } 107 | -------------------------------------------------------------------------------- /pkg/readers/file_test.go: -------------------------------------------------------------------------------- 1 | package readers 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestUrlsFor(t *testing.T) { 9 | fr := FileReader{ 10 | Options: &FileReaderOptions{}, 11 | } 12 | 13 | tests := []struct { 14 | name string 15 | candidate string 16 | ports []int 17 | want []string 18 | }{ 19 | { 20 | name: "Test with IP", 21 | candidate: "192.168.1.1", 22 | ports: []int{80, 443, 8443}, 23 | want: []string{ 24 | "http://192.168.1.1:80", 25 | "http://192.168.1.1:443", 26 | "http://192.168.1.1:8443", 27 | "https://192.168.1.1:80", 28 | "https://192.168.1.1:443", 29 | "https://192.168.1.1:8443", 30 | }, 31 | }, 32 | { 33 | name: "Test with IP and port", 34 | candidate: "192.168.1.1:8080", 35 | ports: []int{80, 443, 8443}, 36 | want: []string{ 37 | "http://192.168.1.1:8080", 38 | "https://192.168.1.1:8080", 39 | }, 40 | }, 41 | { 42 | name: "Test with IP and port with spaces", 43 | candidate: " 192.168.1.1:8080 ", 44 | ports: []int{80, 443, 8443}, 45 | want: []string{ 46 | "http://192.168.1.1:8080", 47 | "https://192.168.1.1:8080", 48 | }, 49 | }, 50 | { 51 | name: "Test with scheme, IP and port", 52 | candidate: "http://192.168.1.1:8080", 53 | ports: []int{80, 443, 8443}, 54 | want: []string{ 55 | "http://192.168.1.1:8080", 56 | }, 57 | }, 58 | { 59 | name: "Test with scheme and IP", 60 | candidate: "https://192.168.1.1", 61 | ports: []int{80, 443, 8443}, 62 | want: []string{ 63 | "https://192.168.1.1:80", 64 | "https://192.168.1.1:443", 65 | "https://192.168.1.1:8443", 66 | }, 67 | }, 68 | { 69 | name: "Test with IP and path", 70 | candidate: "192.168.1.1/path", 71 | ports: []int{80, 443, 8443}, 72 | want: []string{ 73 | "http://192.168.1.1:80/path", 74 | "http://192.168.1.1:443/path", 75 | "http://192.168.1.1:8443/path", 76 | "https://192.168.1.1:80/path", 77 | "https://192.168.1.1:443/path", 78 | "https://192.168.1.1:8443/path", 79 | }, 80 | }, 81 | { 82 | name: "Test with scheme, IP, port and path", 83 | candidate: "http://192.168.1.1:8080/path", 84 | ports: []int{80, 443, 8443}, 85 | want: []string{ 86 | "http://192.168.1.1:8080/path", 87 | }, 88 | }, 89 | { 90 | name: "Test with IP, port and path", 91 | candidate: "192.168.1.1:8080/path", 92 | ports: []int{80, 443, 8443}, 93 | want: []string{ 94 | "http://192.168.1.1:8080/path", 95 | "https://192.168.1.1:8080/path", 96 | }, 97 | }, 98 | } 99 | 100 | for _, tt := range tests { 101 | t.Run(tt.name, func(t *testing.T) { 102 | got := fr.urlsFor(tt.candidate, tt.ports) 103 | if !reflect.DeepEqual(got, tt.want) { 104 | t.Errorf("urlsFor() =>\n\nhave: %v\nwant %v", got, tt.want) 105 | } 106 | }) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /pkg/readers/reader.go: -------------------------------------------------------------------------------- 1 | package readers 2 | 3 | // Reader defines a reader. 4 | // NOTE: The Reader needs to close the channel when done to stop the runner. 5 | // You would typically do this with a "defer close(ch)" at the start of your 6 | // Read() implementation. 7 | type Reader interface { 8 | Read(chan<- string) error 9 | } 10 | 11 | // port collections that readers can refer to 12 | var ( 13 | small = []int{8080, 8443} 14 | medium = append(small, []int{81, 90, 591, 3000, 3128, 8000, 8008, 8081, 8082, 8834, 8888, 7015, 8800, 8990, 10000}...) 15 | large = append(medium, []int{300, 2082, 2087, 2095, 4243, 4993, 5000, 7000, 7171, 7396, 7474, 8090, 8280, 8880, 9443}...) 16 | ) 17 | -------------------------------------------------------------------------------- /pkg/runner/options.go: -------------------------------------------------------------------------------- 1 | package runner 2 | 3 | import ( 4 | "net/url" 5 | ) 6 | 7 | // Options are global github.com/helviojunior/enumdnsenumdns options 8 | type Options struct { 9 | // Logging is logging options 10 | Logging Logging 11 | // DNS Over HTTPs related options 12 | DnsOverHttps DnsOverHttps 13 | // Writer is output options 14 | Writer Writer 15 | // Scan is typically Scan options 16 | Scan Scan 17 | // 18 | DnsSuffix string 19 | 20 | // 21 | DnsServer string 22 | DnsPort int 23 | DnsProtocol string 24 | PrivateDns bool 25 | 26 | Proxy *url.URL 27 | 28 | Quick bool 29 | StoreTempAsWorkspace bool 30 | LocalWorkspace bool 31 | } 32 | 33 | // Logging is log related options 34 | type Logging struct { 35 | // Debug display debug level logging 36 | Debug bool 37 | // LogScanErrors log errors related to scanning 38 | LogScanErrors bool 39 | // Silence all logging 40 | Silence bool 41 | } 42 | 43 | // Writer options 44 | type Writer struct { 45 | UserPath string 46 | Db bool 47 | DbURI string 48 | DbDebug bool // enables verbose database logs 49 | Csv bool 50 | CsvFile string 51 | Jsonl bool 52 | JsonlFile string 53 | ELastic bool 54 | ELasticURI string 55 | Text bool 56 | TextFile string 57 | Stdout bool 58 | None bool 59 | NoControlDb bool 60 | CtrlDbURI string 61 | } 62 | 63 | // DNS Over HTTPs related options 64 | type DnsOverHttps struct { 65 | 66 | // Don't write HTML response content 67 | SkipSSLCheck bool 68 | 69 | // Proxy server to use 70 | Proxy string 71 | 72 | ProxyUser string 73 | ProxyPassword string 74 | 75 | // UserAgent is the user-agent string to set for Chrome 76 | UserAgent string 77 | // Headers to add to every request 78 | Headers []string 79 | 80 | } 81 | 82 | // Scan is scanning related options 83 | type Scan struct { 84 | // Threads (not really) are the number of goroutines to use. 85 | // More soecifically, its the go-rod page pool well use. 86 | Threads int 87 | // Timeout is the maximum time to wait for a page load before timing out. 88 | Timeout int 89 | // Number of seconds of delay between navigation and screenshotting 90 | Delay int 91 | } 92 | 93 | // NewDefaultOptions returns Options with some default values 94 | func NewDefaultOptions() *Options { 95 | return &Options{ 96 | Scan: Scan{ 97 | Threads: 6, 98 | Timeout: 60, 99 | }, 100 | Logging: Logging{ 101 | Debug: true, 102 | LogScanErrors: true, 103 | }, 104 | DnsSuffix: "", 105 | Quick: false, 106 | PrivateDns: false, 107 | } 108 | } -------------------------------------------------------------------------------- /pkg/runner/products.go: -------------------------------------------------------------------------------- 1 | package runner 2 | 3 | var products = map[string][]string{ 4 | "CloudFront": []string{ "cloudfront.net", "cloudfront", }, 5 | "CloudFlare": []string{ "cloudflare.com", "cloudflare", "cc-ecdn.net" }, 6 | "Akamai": []string{ "akamaitechnologies.com", "edgekey.net", "akamaiedge.net", "akam.net" }, 7 | "Imperva": []string{ "incapsula.com" }, 8 | "Sucuri": []string{ "sucuri.net" }, 9 | "Bunny": []string{ "bunnyinfra.net" }, 10 | "KeyCDN": []string{ "proinity.net" }, 11 | "CDN77": []string{ "cdn77.com" }, 12 | "AWS Global Accelerator": []string{ "awsglobalaccelerator.com", }, 13 | "AWS": []string{ "amazonaws.com", "awsdns", }, 14 | "Microsoft Office 365": []string{ "lync.com", "office.com", "outlook.com" }, 15 | "Microsoft Sharepoint": []string{ "sharepointonline.com" }, 16 | "Azure": []string{ "azure-dns.com", "azure-dns.net", "azure-dns.org", "azure-dns.info", "azurewebsites.net", "cloudapp.net" }, 17 | "Oracle Cloud": []string{ "oraclecloud.net" }, 18 | "GCP": []string{ "googleusercontent.com" }, 19 | "Registro.BR": []string { "dns.br" }, 20 | } 21 | 22 | var saas_products = map[string][]string{ 23 | "CloudFront": []string{ "cloudfront.net", "cloudfront", }, 24 | "CloudFlare": []string{ "cloudflare.com", "cloudflare", "cc-ecdn.net" }, 25 | "Akamai": []string{ "akamaitechnologies.com", "edgekey.net", "akamaiedge.net", "akam.net" }, 26 | "Imperva": []string{ "incapsula.com" }, 27 | "Sucuri": []string{ "sucuri.net" }, 28 | "Bunny": []string{ "bunnyinfra.net" }, 29 | "KeyCDN": []string{ "proinity.net" }, 30 | "CDN77": []string{ "cdn77.com" }, 31 | "AWS Global Accelerator": []string{ "awsglobalaccelerator.com", }, 32 | "Microsoft Office 365": []string{ "lync.com", "office.com", "outlook.com" }, 33 | "Microsoft Sharepoint": []string{ "sharepointonline.com" }, 34 | "Azure": []string{ "azure-dns.com", "azure-dns.net", "azure-dns.org", "azure-dns.info", "azurewebsites.net" }, 35 | "Heroku": []string{ "herokuapp.com", "herokudns.com" }, 36 | "Registro.BR": []string { "dns.br" }, 37 | "Trend Micro Email Security": []string { "tmes.trendmicro.com" }, 38 | "Wix": []string{ "wixsite.com", "wixdns.net" }, 39 | "Github": []string{ "github.io", "github.com" }, 40 | "SalesForce": []string{ "exacttarget.com" }, 41 | "Shopify": []string{ "myshopify.com" }, 42 | } 43 | 44 | var datacenter = map[string][]string{ 45 | "ALog": []string{ "alog.com.br", }, 46 | "Toweb": []string{ "datacenter1.com.br", }, 47 | "Uni5": []string{ "uni5.net", }, 48 | "Hosting Service": []string{ "hostingservice.com", }, 49 | "Locaweb": []string{ "locaweb.com.br" }, 50 | "Equinix": []string{ "equinix.com" }, 51 | "Telefonica": []string{ "tdatabrasil.net.br" }, 52 | "UOL": []string{ "uoldiveo.com.br", "compasso.com.br" }, 53 | "HostGator": []string{ "hostgator.com.br" }, 54 | "Datacom": []string{ "dialhost.com.br", "stackpath.net" }, 55 | "DialHost": []string{ "brascloud.com.br", "dialhost.com.br" }, 56 | } 57 | -------------------------------------------------------------------------------- /pkg/writers/csv.go: -------------------------------------------------------------------------------- 1 | package writers 2 | 3 | import ( 4 | "encoding/csv" 5 | "fmt" 6 | "os" 7 | "reflect" 8 | 9 | "github.com/helviojunior/enumdns/internal/tools" 10 | "github.com/helviojunior/enumdns/pkg/models" 11 | ) 12 | 13 | // fields in the main model to ignore 14 | var csvExludedFields = []string{"HTML"} 15 | 16 | // CsvWriter writes CSV files 17 | type CsvWriter struct { 18 | FilePath string 19 | finalPath string 20 | } 21 | 22 | // NewCsvWriter gets a new CsvWriter 23 | func NewCsvWriter(destination string) (*CsvWriter, error) { 24 | p, err := tools.CreateFileWithDir(destination) 25 | if err != nil { 26 | return nil, err 27 | } 28 | 29 | // open the file and write the CSV headers to it 30 | file, err := os.OpenFile(p, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) 31 | if err != nil { 32 | return nil, err 33 | } 34 | 35 | writer := csv.NewWriter(file) 36 | defer writer.Flush() 37 | 38 | if err := writer.Write(csvHeaders()); err != nil { 39 | return nil, err 40 | } 41 | 42 | return &CsvWriter{ 43 | FilePath: destination, 44 | finalPath: p, 45 | }, nil 46 | } 47 | 48 | func (cw *CsvWriter) Finish() error { 49 | return nil 50 | } 51 | 52 | // Write a CSV line 53 | func (cw *CsvWriter) Write(result *models.Result) error { 54 | 55 | if !result.Exists { 56 | return nil 57 | } 58 | 59 | file, err := os.OpenFile(cw.finalPath, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0644) 60 | if err != nil { 61 | return err 62 | } 63 | defer file.Close() 64 | 65 | writer := csv.NewWriter(file) 66 | defer writer.Flush() 67 | 68 | // get values from the result 69 | val := reflect.ValueOf(*result) 70 | numField := val.NumField() 71 | 72 | var values []string 73 | for i := 0; i < numField; i++ { 74 | // skip excluded fields 75 | if tools.SliceHasStr(csvExludedFields, val.Type().Field(i).Name) { 76 | continue 77 | } 78 | 79 | // skip slices 80 | if val.Field(i).Kind() == reflect.Slice { 81 | continue // Optionally skip slice fields, or handle them differently 82 | } 83 | 84 | values = append(values, fmt.Sprintf("%v", val.Field(i).Interface())) 85 | } 86 | 87 | return writer.Write(values) 88 | } 89 | 90 | func (cw *CsvWriter) WriteFqdn(result *models.FQDNData) error { 91 | return nil 92 | } 93 | 94 | // headers returns the headers a CSV file should have. 95 | func csvHeaders() []string { 96 | val := reflect.ValueOf(models.Result{}) 97 | numField := val.NumField() 98 | 99 | var fieldNames []string 100 | for i := 0; i < numField; i++ { 101 | // skip excluded fields 102 | if tools.SliceHasStr(csvExludedFields, val.Type().Field(i).Name) { 103 | continue 104 | } 105 | 106 | // skip slices 107 | if val.Field(i).Kind() == reflect.Slice { 108 | continue // Optionally skip slice fields, or handle them differently 109 | } 110 | 111 | fieldNames = append(fieldNames, val.Type().Field(i).Name) 112 | } 113 | 114 | return fieldNames 115 | } 116 | -------------------------------------------------------------------------------- /pkg/writers/db.go: -------------------------------------------------------------------------------- 1 | package writers 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/helviojunior/enumdns/pkg/database" 7 | "github.com/helviojunior/enumdns/pkg/models" 8 | "gorm.io/gorm" 9 | ) 10 | 11 | var hammingThreshold = 10 12 | var regThreshold = 200 13 | 14 | // DbWriter is a Database writer 15 | type DbWriter struct { 16 | URI string 17 | conn *gorm.DB 18 | mutex sync.Mutex 19 | registers []models.Result 20 | } 21 | 22 | // NewDbWriter initialises a database writer 23 | func NewDbWriter(uri string, debug bool) (*DbWriter, error) { 24 | c, err := database.Connection(uri, false, debug) 25 | if err != nil { 26 | return nil, err 27 | } 28 | /* 29 | if _, ok := c.Statement.Clauses["ON CONFLICT"]; !ok { 30 | c = c.Clauses(clause.OnConflict{UpdateAll: true}) 31 | }*/ 32 | 33 | return &DbWriter{ 34 | URI: uri, 35 | conn: c, 36 | mutex: sync.Mutex{}, 37 | registers: []models.Result{}, 38 | }, nil 39 | } 40 | 41 | // Write results to the database 42 | func (dw *DbWriter) Write(result *models.Result) error { 43 | dw.mutex.Lock() 44 | defer dw.mutex.Unlock() 45 | var err error 46 | 47 | if !result.Exists { 48 | dw.registers = append(dw.registers, *result) 49 | if len(dw.registers) >= regThreshold { 50 | err = dw.conn.CreateInBatches(dw.registers, 50).Error 51 | dw.registers = []models.Result{} 52 | } 53 | }else{ 54 | err = dw.conn.CreateInBatches(result, 50).Error 55 | 56 | //err = dw.conn.Table("results").CreateInBatches( []models.Result{ *result }, 50).Error 57 | 58 | fqdn := result.ToFqdn() 59 | if fqdn != nil { 60 | // Not call WriteFqdn function because it will cause an deadlock at mutex 61 | err1 := dw.conn.CreateInBatches(fqdn, 50).Error 62 | if err1 != nil && err == nil{ 63 | err = err1 64 | } 65 | } 66 | 67 | } 68 | 69 | return err 70 | } 71 | 72 | func (dw *DbWriter) WriteFqdn(fqdn *models.FQDNData) error { 73 | dw.mutex.Lock() 74 | defer dw.mutex.Unlock() 75 | 76 | return dw.conn.Create(fqdn).Error 77 | } 78 | 79 | func (dw *DbWriter) Finish() error { 80 | return nil 81 | } 82 | 83 | -------------------------------------------------------------------------------- /pkg/writers/elastic.go: -------------------------------------------------------------------------------- 1 | package writers 2 | 3 | import ( 4 | "encoding/json" 5 | "time" 6 | "net/url" 7 | "math" 8 | "fmt" 9 | "strings" 10 | "context" 11 | "errors" 12 | "bytes" 13 | "net/http" 14 | //"path/filepath" 15 | //"reflect" 16 | //"io" 17 | 18 | "github.com/helviojunior/enumdns/internal/tools" 19 | "github.com/helviojunior/enumdns/pkg/models" 20 | elk "github.com/elastic/go-elasticsearch/v8" 21 | esapi "github.com/elastic/go-elasticsearch/v8/esapi" 22 | logger "github.com/helviojunior/enumdns/pkg/log" 23 | ) 24 | 25 | // fields in the main model to ignore 26 | var elkExludedFields = []string{"network"} 27 | var elkBulkCount = 1000 28 | var elkBulkMaxSize = 5 * 1024 * 1024 29 | 30 | // JsonWriter is a JSON lines writer 31 | type ElasticWriter struct { 32 | Client *elk.Client 33 | Index string 34 | screenshotPath string 35 | } 36 | 37 | type bulkResponse struct { 38 | Errors bool `json:"errors"` 39 | Items []struct { 40 | Index struct { 41 | ID string `json:"_id"` 42 | Result string `json:"result"` 43 | Status int `json:"status"` 44 | Error struct { 45 | Type string `json:"type"` 46 | Reason string `json:"reason"` 47 | Cause struct { 48 | Type string `json:"type"` 49 | Reason string `json:"reason"` 50 | } `json:"caused_by"` 51 | } `json:"error"` 52 | } `json:"index"` 53 | } `json:"items"` 54 | } 55 | 56 | type indexResponse struct { 57 | ID string `json:"_id"` 58 | Index string `json:"_index"` 59 | Result string `json:"result"` 60 | Error struct { 61 | Type string `json:"type"` 62 | Reason string `json:"reason"` 63 | Cause struct { 64 | Type string `json:"type"` 65 | Reason string `json:"reason"` 66 | } `json:"caused_by"` 67 | } `json:"error"` 68 | } 69 | 70 | // NewJsonWriter return a new Json lines writer 71 | func NewElasticWriter(uri string) (*ElasticWriter, error) { 72 | 73 | u, err := url.Parse(uri) 74 | if err != nil { 75 | return nil, err 76 | } 77 | 78 | username := u.User.Username() 79 | password, _ := u.User.Password() 80 | port := u.Port() 81 | if port == "" { 82 | port = "9200" 83 | } 84 | index_name := u.EscapedPath() 85 | index_name = strings.Trim(index_name, "/ ") 86 | index_name = strings.SplitN(index_name, "/", 2)[0] 87 | if index_name == "" { 88 | index_name = "enumdns" 89 | } 90 | 91 | wr := &ElasticWriter{ 92 | Index: index_name, 93 | screenshotPath: "", 94 | } 95 | 96 | conf := elk.Config{ 97 | Addresses: []string{ 98 | fmt.Sprintf("%s://%s:%s/", u.Scheme, u.Hostname(), port), 99 | }, 100 | //Username: username, 101 | //Password: password, 102 | //CACert: cert, 103 | RetryOnStatus: []int{429, 502, 503, 504}, 104 | RetryBackoff: func(i int) time.Duration { 105 | // A simple exponential delay 106 | d := time.Duration(math.Exp2(float64(i))) * time.Second 107 | logger.Debugf("Elastic retry, attempt: %d | Sleeping for %s...\n", i, d) 108 | return d 109 | }, 110 | Transport: &http.Transport{ 111 | MaxIdleConns: 10, 112 | IdleConnTimeout: 10 * time.Second, 113 | DisableCompression: true, 114 | }, 115 | } 116 | 117 | if username != "" && password != "" { 118 | conf.Username = username 119 | conf.Password = password 120 | } 121 | 122 | wr.Client, err = elk.NewClient(conf) 123 | if err != nil { 124 | return nil, err 125 | } 126 | 127 | //File Index 128 | err = wr.CreateIndex(wr.Index, `{ 129 | "settings": { 130 | "number_of_replicas": 1, 131 | "index": {"highlight.max_analyzed_offset": 10000000} 132 | }, 133 | 134 | "mappings": { 135 | "properties": { 136 | "probed_at": {"type": "date"}, 137 | "test_id": {"type": "keyword"}, 138 | "fqdn": {"type": "keyword"}, 139 | "result_type": {"type": "keyword"}, 140 | "ipv4": {"type": "keyword"}, 141 | "ipv6": {"type": "keyword"}, 142 | "target": {"type": "keyword"}, 143 | "ptr": {"type": "keyword"}, 144 | "failed": {"type": "keyword"}, 145 | "failed_reason": {"type": "text"} 146 | } 147 | } 148 | }`) 149 | if err != nil { 150 | return nil, err 151 | } 152 | 153 | return wr, nil 154 | } 155 | 156 | // Write JSON lines to a file 157 | func (ew *ElasticWriter) Write(result *models.Result) error { 158 | 159 | if !result.Exists { 160 | return nil 161 | } 162 | 163 | //File 164 | b_data, err := json.Marshal(*result) //ew.Marshal(*result) 165 | if err != nil { 166 | return err 167 | } 168 | 169 | res, err := ew.Client.Index(ew.Index, bytes.NewReader(b_data), ew.Client.Index.WithDocumentID(result.GetHash())) 170 | if err != nil { 171 | return err 172 | } 173 | if res.StatusCode != 200 && res.StatusCode != 201 { 174 | fmt.Printf("Err: %s", res) 175 | return errors.New("Cannot create/update document") 176 | } 177 | 178 | return nil 179 | } 180 | 181 | func (ew *ElasticWriter) CreateIndex(index string, mapping string) error { 182 | 183 | var raw map[string]interface{} 184 | 185 | response, err := ew.Client.Indices.Exists([]string{index}) 186 | if err != nil { 187 | return err 188 | } 189 | defer response.Body.Close() 190 | 191 | if response.IsError() { 192 | 193 | if response.StatusCode == 404 { 194 | indexReq := esapi.IndicesCreateRequest{ 195 | Index: index, 196 | Body: strings.NewReader(string(mapping)), 197 | } 198 | 199 | logger.Infof("Creating elastic index %s", index) 200 | res, err := indexReq.Do(context.Background(), ew.Client) 201 | if err != nil { 202 | return err 203 | } 204 | defer res.Body.Close() 205 | 206 | if res.IsError() { 207 | 208 | if err := json.NewDecoder(res.Body).Decode(&raw); err != nil { 209 | return errors.New(fmt.Sprintf("Failure to to parse response body: %s", err)) 210 | } else { 211 | return errors.New(fmt.Sprintf("Cannot create/update elastic index [%d] %s: %s", 212 | res.StatusCode, 213 | raw["error"].(map[string]interface{})["type"], 214 | raw["error"].(map[string]interface{})["reason"], 215 | )) 216 | } 217 | 218 | } 219 | 220 | }else{ 221 | 222 | if err := json.NewDecoder(response.Body).Decode(&raw); err != nil { 223 | return errors.New(fmt.Sprintf("Failure to to parse response body: %s", err)) 224 | } else { 225 | return errors.New(fmt.Sprintf("Cannot get elastic index [%d] %s: %s", 226 | response.StatusCode, 227 | raw["error"].(map[string]interface{})["type"], 228 | raw["error"].(map[string]interface{})["reason"], 229 | )) 230 | } 231 | 232 | 233 | } 234 | 235 | } 236 | 237 | return nil 238 | 239 | } 240 | 241 | func (ew *ElasticWriter) CreateDocBulk(index string, docs map[string][]byte) error { 242 | var raw map[string]interface{} 243 | var buf bytes.Buffer 244 | size := 0 245 | for id, doc := range docs { 246 | meta := []byte(fmt.Sprintf(`{ "index" : { "_id" : "%s" } }%s`, id, "\n")) 247 | data := []byte(doc) 248 | data = append(data, "\n"...) 249 | 250 | size += len(meta) + len(data) 251 | buf.Grow(len(meta) + len(data)) 252 | buf.Write(meta) 253 | buf.Write(data) 254 | 255 | } 256 | 257 | logger.Debugf("Elastic bulk %d docs, %d bytes", len(docs), size) 258 | 259 | for i := range 10 { 260 | 261 | res, err := ew.Client.Bulk(bytes.NewReader(buf.Bytes()), ew.Client.Bulk.WithIndex(index)) 262 | if err != nil { 263 | return err 264 | } 265 | defer res.Body.Close() 266 | 267 | if res.IsError() { 268 | 269 | if i >= 5 { 270 | if err := json.NewDecoder(res.Body).Decode(&raw); err != nil { 271 | return errors.New(fmt.Sprintf("Failure to to parse response body: %s", err)) 272 | } else { 273 | return errors.New(fmt.Sprintf("Error: [%d] %s: %s", 274 | res.StatusCode, 275 | raw["error"].(map[string]interface{})["type"], 276 | raw["error"].(map[string]interface{})["reason"], 277 | )) 278 | } 279 | 280 | } 281 | 282 | // A successful response might still contain errors for particular documents... 283 | // 284 | } else { 285 | var blk *bulkResponse 286 | if err := json.NewDecoder(res.Body).Decode(&blk); err != nil { 287 | return errors.New(fmt.Sprintf("Failure to to parse response body: %s", err)) 288 | } else { 289 | for _, d := range blk.Items { 290 | // ... so for any HTTP status above 201 ... 291 | // 292 | if d.Index.Status > 201 { 293 | // ... and print the response status and error information ... 294 | logger.Debugf(" Error: [%d]: %s: %s: %s: %s", 295 | d.Index.Status, 296 | d.Index.Error.Type, 297 | d.Index.Error.Reason, 298 | d.Index.Error.Cause.Type, 299 | d.Index.Error.Cause.Reason, 300 | ) 301 | } else { 302 | // ... otherwise increase the success counter. 303 | // 304 | 305 | } 306 | } 307 | } 308 | } 309 | 310 | if res.StatusCode == 200 || res.StatusCode == 201 { 311 | return nil 312 | } 313 | 314 | time.Sleep(1 * time.Second) 315 | } 316 | 317 | return errors.New("Cannot create/update document") 318 | } 319 | 320 | 321 | func (ew *ElasticWriter) CreateDoc(index string, data []byte, doc_id string) error { 322 | var raw map[string]interface{} 323 | for i := range 10 { 324 | res, err := ew.Client.Index(index, bytes.NewReader(data), ew.Client.Index.WithDocumentID(doc_id)) 325 | if err != nil { 326 | return err 327 | } 328 | defer res.Body.Close() 329 | 330 | if res.IsError() { 331 | 332 | if i >= 5 { 333 | if err := json.NewDecoder(res.Body).Decode(&raw); err != nil { 334 | return errors.New(fmt.Sprintf("Failure to to parse response body: %s", err)) 335 | } else { 336 | return errors.New(fmt.Sprintf("Error: [%d] %s: %s", 337 | res.StatusCode, 338 | raw["error"].(map[string]interface{})["type"], 339 | raw["error"].(map[string]interface{})["reason"], 340 | )) 341 | } 342 | 343 | } 344 | 345 | // A successful response might still contain errors for particular documents... 346 | // 347 | } else { 348 | 349 | if res.StatusCode == 200 || res.StatusCode == 201 { 350 | return nil 351 | } 352 | 353 | //bodyBytes, err := io.ReadAll(res.Body) 354 | //if err != nil { 355 | // return err 356 | //} 357 | //bodyString := string(bodyBytes) 358 | //fmt.Printf("Resp: %s", bodyString) 359 | 360 | var idxRes *indexResponse 361 | 362 | if err := json.NewDecoder(res.Body).Decode(&idxRes); err != nil { 363 | return errors.New(fmt.Sprintf("Failure to to parse response body: %s", err)) 364 | } else { 365 | //Debug result 366 | } 367 | } 368 | 369 | time.Sleep(1 * time.Second) 370 | } 371 | 372 | return errors.New("Cannot create/update document") 373 | } 374 | 375 | func (ew *ElasticWriter) MarshalAppend(marshalled []byte, new_data map[string]interface{}) ([]byte, error) { 376 | t_data := make(map[string]interface{}) 377 | err := json.Unmarshal(marshalled, &t_data) 378 | 379 | data := make(map[string]interface{}) 380 | for k, v := range t_data { 381 | // skip excluded fields 382 | if tools.SliceHasStr(elkExludedFields, k) { 383 | continue 384 | } 385 | 386 | data[k] = v 387 | } 388 | 389 | for k, v := range new_data { 390 | data[k] = v 391 | } 392 | 393 | j_data, err := json.Marshal(data) 394 | if err != nil { 395 | return marshalled, err 396 | } 397 | 398 | return j_data, nil 399 | } 400 | 401 | 402 | func (ew *ElasticWriter) Marshal(v any) ([]byte, error) { 403 | j, err := json.Marshal(v) 404 | if err != nil { 405 | return []byte{}, err 406 | } 407 | 408 | t_data := make(map[string]interface{}) 409 | err = json.Unmarshal(j, &t_data) 410 | 411 | data := make(map[string]interface{}) 412 | for k, v := range t_data { 413 | // skip excluded fields 414 | if tools.SliceHasStr(elkExludedFields, k) { 415 | continue 416 | } 417 | 418 | data[k] = v 419 | } 420 | 421 | j_data, err := json.Marshal(data) 422 | if err != nil { 423 | return []byte{}, err 424 | } 425 | 426 | return j_data[:], nil 427 | } 428 | 429 | func (ew *ElasticWriter) WriteFqdn(result *models.FQDNData) error { 430 | return nil 431 | } 432 | 433 | func (ew *ElasticWriter) Finish() error { 434 | return nil 435 | } 436 | -------------------------------------------------------------------------------- /pkg/writers/json.go: -------------------------------------------------------------------------------- 1 | package writers 2 | 3 | import ( 4 | "encoding/json" 5 | "os" 6 | 7 | "github.com/helviojunior/enumdns/internal/tools" 8 | "github.com/helviojunior/enumdns/pkg/models" 9 | ) 10 | 11 | // JsonWriter is a JSON lines writer 12 | type JsonWriter struct { 13 | FilePath string 14 | } 15 | 16 | // NewJsonWriter return a new Json lines writer 17 | func NewJsonWriter(destination string) (*JsonWriter, error) { 18 | // check if the destination exists, if not, create it 19 | dst, err := tools.CreateFileWithDir(destination) 20 | if err != nil { 21 | return nil, err 22 | } 23 | 24 | return &JsonWriter{ 25 | FilePath: dst, 26 | }, nil 27 | } 28 | 29 | // Write JSON lines to a file 30 | func (jw *JsonWriter) Write(result *models.Result) error { 31 | 32 | if !result.Exists { 33 | return nil 34 | } 35 | 36 | j, err := json.Marshal(result) 37 | if err != nil { 38 | return err 39 | } 40 | 41 | // Open the file in append mode, create it if it doesn't exist 42 | file, err := os.OpenFile(jw.FilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) 43 | if err != nil { 44 | return err 45 | } 46 | defer file.Close() 47 | 48 | // Append the JSON data as a new line 49 | if _, err := file.Write(append(j, '\n')); err != nil { 50 | return err 51 | } 52 | 53 | /* 54 | fqdn := result.ToFqdn() 55 | if fqdn != nil { 56 | jw.WriteFqdn(fqdn) 57 | }*/ 58 | 59 | return nil 60 | } 61 | 62 | 63 | func (jw *JsonWriter) WriteFqdn(result *models.FQDNData) error { 64 | 65 | j, err := json.Marshal(result) 66 | if err != nil { 67 | return err 68 | } 69 | 70 | // Open the file in append mode, create it if it doesn't exist 71 | file, err := os.OpenFile(jw.FilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) 72 | if err != nil { 73 | return err 74 | } 75 | defer file.Close() 76 | 77 | // Append the JSON data as a new line 78 | if _, err := file.Write(append(j, '\n')); err != nil { 79 | return err 80 | } 81 | 82 | return nil 83 | } 84 | 85 | func (jw *JsonWriter)Finish() error { 86 | return nil 87 | } -------------------------------------------------------------------------------- /pkg/writers/memory.go: -------------------------------------------------------------------------------- 1 | package writers 2 | 3 | import ( 4 | "errors" 5 | "sync" 6 | 7 | "github.com/helviojunior/enumdns/pkg/models" 8 | ) 9 | 10 | // MemoryWriter is a memory-based results queue with a maximum slot count 11 | type MemoryWriter struct { 12 | slots int 13 | results []*models.Result 14 | mutex sync.Mutex 15 | } 16 | 17 | // NewMemoryWriter initializes a MemoryWriter with the specified number of slots 18 | func NewMemoryWriter(slots int) (*MemoryWriter, error) { 19 | if slots <= 0 { 20 | return nil, errors.New("slots need to be a positive integer") 21 | } 22 | 23 | return &MemoryWriter{ 24 | slots: slots, 25 | results: make([]*models.Result, 0, slots), 26 | mutex: sync.Mutex{}, 27 | }, nil 28 | } 29 | 30 | // Write adds a new result to the MemoryWriter. 31 | func (s *MemoryWriter) Write(result *models.Result) error { 32 | s.mutex.Lock() 33 | defer s.mutex.Unlock() 34 | 35 | if len(s.results) >= s.slots { 36 | s.results = s.results[1:] 37 | } 38 | 39 | s.results = append(s.results, result) 40 | 41 | return nil 42 | } 43 | 44 | // GetLatest retrieves the most recently added result. 45 | func (s *MemoryWriter) GetLatest() *models.Result { 46 | s.mutex.Lock() 47 | defer s.mutex.Unlock() 48 | 49 | if len(s.results) == 0 { 50 | return nil 51 | } 52 | 53 | return s.results[len(s.results)-1] 54 | } 55 | 56 | // GetFirst retrieves the oldest result in the MemoryWriter. 57 | func (s *MemoryWriter) GetFirst() *models.Result { 58 | s.mutex.Lock() 59 | defer s.mutex.Unlock() 60 | 61 | if len(s.results) == 0 { 62 | return nil 63 | } 64 | 65 | return s.results[0] 66 | } 67 | 68 | // GetAllResults returns a copy of all current results. 69 | func (s *MemoryWriter) GetAllResults() []*models.Result { 70 | s.mutex.Lock() 71 | defer s.mutex.Unlock() 72 | 73 | // Create a copy to prevent external modification 74 | resultsCopy := make([]*models.Result, len(s.results)) 75 | copy(resultsCopy, s.results) 76 | 77 | return resultsCopy 78 | } 79 | 80 | func (s *MemoryWriter) WriteFqdn(result *models.FQDNData) error { 81 | return nil 82 | } 83 | 84 | 85 | func (s *MemoryWriter) Finish() error { 86 | return nil 87 | } -------------------------------------------------------------------------------- /pkg/writers/none.go: -------------------------------------------------------------------------------- 1 | package writers 2 | 3 | import ( 4 | "github.com/helviojunior/enumdns/pkg/models" 5 | ) 6 | 7 | // NoneWriter is a None writer 8 | type NoneWriter struct { 9 | } 10 | 11 | // NewNoneWriter initialises a none writer 12 | func NewNoneWriter() (*NoneWriter, error) { 13 | return &NoneWriter{}, nil 14 | } 15 | 16 | // Write does nothing 17 | func (s *NoneWriter) Write(result *models.Result) error { 18 | return nil 19 | } 20 | 21 | func (s *NoneWriter)WriteFqdn(result *models.FQDNData) error { 22 | return nil 23 | } 24 | 25 | func (s *NoneWriter) Finish() error { 26 | return nil 27 | } -------------------------------------------------------------------------------- /pkg/writers/stdout.go: -------------------------------------------------------------------------------- 1 | package writers 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "golang.org/x/term" 8 | 9 | "github.com/helviojunior/enumdns/pkg/models" 10 | logger "github.com/helviojunior/enumdns/pkg/log" 11 | ) 12 | 13 | // StdoutWriter is a Stdout writer 14 | type StdoutWriter struct { 15 | WriteAll bool 16 | IsTerminal bool 17 | } 18 | 19 | // NewStdoutWriter initialises a stdout writer 20 | func NewStdoutWriter() (*StdoutWriter, error) { 21 | return &StdoutWriter{ 22 | WriteAll: false, 23 | IsTerminal: term.IsTerminal(int(os.Stdin.Fd())), 24 | }, nil 25 | } 26 | 27 | // Write results to stdout 28 | func (s *StdoutWriter) Write(result *models.Result) error { 29 | if s.IsTerminal { 30 | fmt.Fprintf(os.Stderr, " \r") 31 | } 32 | if result.Failed { 33 | logger.Errorf("[%s] FQDN=%s", result.FailedReason, result.FQDN) 34 | return nil 35 | } 36 | 37 | if !result.Exists { 38 | return nil 39 | } 40 | 41 | if s.WriteAll { 42 | switch result.RType { 43 | case "A", "AAAA": 44 | logger.Infof("%s", result.String()) 45 | case "SOA": 46 | if result.FQDN != result.Target { 47 | logger.Infof("%s", result.String()) 48 | } 49 | default: 50 | logger.Infof("%s", result.String()) 51 | } 52 | }else{ 53 | switch result.RType { 54 | case "A", "AAAA": 55 | logger.Infof("%s", result.String()) 56 | default: 57 | logger.Debug(result.String()) 58 | } 59 | } 60 | 61 | return nil 62 | } 63 | 64 | func (s *StdoutWriter) WriteFqdn(result *models.FQDNData) error { 65 | return nil 66 | } 67 | 68 | func (s *StdoutWriter) Finish() error { 69 | return nil 70 | } -------------------------------------------------------------------------------- /pkg/writers/text.go: -------------------------------------------------------------------------------- 1 | package writers 2 | 3 | import ( 4 | "time" 5 | "os" 6 | "strings" 7 | 8 | "github.com/helviojunior/enumdns/pkg/models" 9 | ) 10 | 11 | // StdoutWriter is a Stdout writer 12 | type TextWriter struct { 13 | FilePath string 14 | finalPath string 15 | } 16 | 17 | // NewStdoutWriter initialises a stdout writer 18 | func NewTextWriter(destination string) (*TextWriter, error) { 19 | // open the file and write the CSV headers to it 20 | file, err := os.OpenFile(destination, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644) 21 | if err != nil { 22 | return nil, err 23 | } 24 | defer file.Close() 25 | 26 | if _, err := file.WriteString(txtHeader()); err != nil { 27 | return nil, err 28 | } 29 | 30 | return &TextWriter{ 31 | FilePath: destination, 32 | finalPath: destination, 33 | }, nil 34 | } 35 | 36 | func txtHeader() string { 37 | txt := "######################################\r\n## Date: " + time.Now().Format(time.RFC3339) + "\r\n\r\n" 38 | txt += "FQDN" + strings.Repeat(" ", 67) 39 | txt += "Type" + strings.Repeat(" ", 7) 40 | txt += "Value" + strings.Repeat(" ", 50) 41 | txt += "\r\n" 42 | txt += strings.Repeat("=", 70) + " " 43 | txt += strings.Repeat("=", 10) + " " 44 | txt += strings.Repeat("=", 50) 45 | txt += "\r\n" 46 | 47 | return txt 48 | } 49 | 50 | func (t *TextWriter) Finish() error { 51 | file, err := os.OpenFile(t.finalPath, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644) 52 | if err != nil { 53 | return err 54 | } 55 | defer file.Close() 56 | 57 | if _, err := file.WriteString("\r\nFinished at: " + time.Now().Format(time.RFC3339) + "\r\n\r\n"); err != nil { 58 | return err 59 | } 60 | 61 | return nil 62 | } 63 | 64 | // Write results to stdout 65 | func (t *TextWriter) Write(result *models.Result) error { 66 | 67 | if !result.Exists { 68 | return nil 69 | } 70 | 71 | file, err := os.OpenFile(t.finalPath, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644) 72 | if err != nil { 73 | return err 74 | } 75 | defer file.Close() 76 | 77 | if _, err := file.WriteString(t.formatResult(result) + "\r\n"); err != nil { 78 | return err 79 | } 80 | 81 | return nil 82 | } 83 | 84 | func (t *TextWriter) WriteFqdn(result *models.FQDNData) error { 85 | return nil 86 | } 87 | 88 | 89 | func (t *TextWriter) formatResult(result *models.Result) string { 90 | 91 | r := strings.Trim(strings.ToLower(result.FQDN), ". ") 92 | s := 71 - len(strings.Trim(strings.ToLower(result.FQDN), ". ")) 93 | if s <= 0 { 94 | s = 1 95 | } 96 | r += strings.Repeat(" ", s) 97 | 98 | r += result.RType 99 | s = 11 - len(result.RType) 100 | if s <= 0 { 101 | s = 1 102 | } 103 | r += strings.Repeat(" ", s) 104 | 105 | switch result.RType { 106 | case "A": 107 | r += result.IPv4 108 | case "AAAA": 109 | r += result.IPv6 110 | case "CNAME", "SRV", "NS", "SOA", "MX": 111 | r += strings.Trim(strings.ToLower(result.Target), ". ") 112 | case "PTR": 113 | r += strings.Trim(strings.ToLower(result.Ptr), ". ") + " -> " 114 | if result.IPv6 != "" { 115 | r += result.IPv6 116 | }else{ 117 | r += result.IPv4 118 | } 119 | case "TXT": 120 | r += result.Txt 121 | default: 122 | r = r + result.RType + " " 123 | if result.IPv6 != "" { 124 | r += result.IPv6 125 | }else if result.IPv4 != "" { 126 | r += result.IPv4 127 | }else if result.Target != "" { 128 | r += strings.Trim(strings.ToLower(result.Target), ". ") 129 | }else if result.Ptr != "" { 130 | r += result.Ptr 131 | } 132 | } 133 | if result.CloudProduct != "" || result.SaaSProduct != "" || result.Datacenter != "" { 134 | prod := "" 135 | 136 | if result.CloudProduct != "" { 137 | prod += "Cloud = " + result.CloudProduct 138 | } 139 | if result.SaaSProduct != "" { 140 | if prod != "" { 141 | prod += ", " 142 | } 143 | prod += "SaaS = " + result.SaaSProduct 144 | } 145 | if result.Datacenter != "" { 146 | if prod != "" { 147 | prod += ", " 148 | } 149 | prod += "Datacenter = " + result.Datacenter 150 | } 151 | 152 | r += " (" + prod + ")" 153 | } 154 | if result.DC || result.GC { 155 | ad := []string{} 156 | if result.GC { 157 | ad = append(ad, "GC") 158 | } 159 | if result.DC { 160 | ad = append(ad, "DC") 161 | } 162 | r += " (" + strings.Join(ad, ", ") + ")" 163 | } 164 | return r 165 | } -------------------------------------------------------------------------------- /pkg/writers/writer.go: -------------------------------------------------------------------------------- 1 | package writers 2 | 3 | import "github.com/helviojunior/enumdns/pkg/models" 4 | 5 | // Writer is a results writer 6 | type Writer interface { 7 | Write(*models.Result) error 8 | WriteFqdn(*models.FQDNData) error 9 | Finish() error 10 | } 11 | --------------------------------------------------------------------------------