Skip to content

Commit 1dcea6c

Browse files
Merge pull request #4 from cyclone-github/testing
Fix issues 1, 2 & 3
2 parents 7f88a9f + 0fd6cd0 commit 1dcea6c

File tree

4 files changed

+130
-79
lines changed

4 files changed

+130
-79
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
### version 0.2.4; 2025-01-08
2+
```
3+
added -o {output_file} flag to redirect stdout to file
4+
updated -help output
5+
refactored code
6+
```
17
### version 0.2.3; 2025-01-07
28
```
39
add sanity check for punycode domains, https://github.com/cyclone-github/ipscope/issues/1

README.md

Lines changed: 53 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,14 @@
88

99
# IPScope
1010

11-
A CLI tool written in pure Go for IP lookup and subdomain discovery. Designed for security researchers and network administrators to resolve IP addresses for TLDs and subdomains. Includes support for some reverse proxy and WAF detection.
11+
A CLI tool written in pure Go for subdomain discovery and IP lookup. Designed for security researchers and network administrators to resolve IP addresses for TLDs and subdomains. Includes support for some reverse proxy and WAF detection.
1212

13-
IPScope was written as a capable, no-fuss alternative to more complex CLI tools commonly used for subdomain discovery and active DNS resolution. IPScope features a simple CLI that only requires one command-line argument, the target URL, while maintaining a powerful backend and optional command-line arguments for further customization. Since it's written in Go, there's no need to hunt down outdated or obscure Python / Ruby dependencies, and since it's written with ease of use in mind, there's no need to figure out complex command-line arguments -- **IPScope just works**.
13+
IPScope was written as a capable, no-fuss alternative to more complex CLI tools commonly used for subdomain discovery and active DNS resolution. IPScope features a simple CLI that only requires one command-line argument, the target URL, while maintaining a powerful backend and optional command-line arguments for further customization. Since it's written in Go, there's no need to hunt down outdated or obscure Python / Ruby / Perl dependencies (we've all been there), and since it's written with ease of use in mind, there's no need to figure out complex command-line arguments -- **IPScope just works**.
1414

1515
### Usage Instructions:
1616
Of course, don't run IPScope on domains you don't have permission to probe.
1717

18-
- Example Usage:
19-
- `./ipscope.bin -url example.org`
18+
- `./ipscope.bin -url example.org`
2019
```
2120
_
2221
____ _ _ ____| | ___ ____ _____
@@ -25,25 +24,66 @@ Of course, don't run IPScope on domains you don't have permission to probe.
2524
\____)\__ |\____)\_)___/|_| |_|_____)
2625
(____/
2726
27+
Cyclone's IPScope v0.2.4; 2025-01-08
28+
https://github.com/cyclone-github/ipscope
29+
2830
Processing URL: example.org using DNS: 1.1.1.1
2931
3032
TLD example.org 93.184.215.14 AS15133 Edgecast Inc. Dźwirzyno, West Pomerania, PL (Reverse Proxy or WAF Detected)
3133
TLD www.example.org 93.184.215.14 AS15133 Edgecast Inc. Dźwirzyno, West Pomerania, PL (Reverse Proxy or WAF Detected)
3234
```
33-
- `./ipscope.bin -url example.org -sub subdomains.txt -dns 8.8.8.8`
35+
- `./ipscope.bin -url example.org -sub subdomains.txt -dns 8.8.8.8 -json`
36+
```
37+
_
38+
____ _ _ ____| | ___ ____ _____
39+
/ ___) | | |/ ___) |/ _ \| _ \| ___ |
40+
( (___| |_| ( (___| | |_| | | | | ____|
41+
\____)\__ |\____)\_)___/|_| |_|_____)
42+
(____/
43+
44+
Cyclone's IPScope v0.2.4; 2025-01-08
45+
https://github.com/cyclone-github/ipscope
46+
47+
Processing URL: example.org using DNS: 8.8.8.8
48+
49+
{"label":"TLD","domain":"www.example.org","ip":"93.184.215.14","asn":"AS15133 Edgecast Inc.","city":"Dźwirzyno","region":"West Pomerania","country":"PL","proxy":true}
50+
{"label":"TLD","domain":"example.org","ip":"93.184.215.14","asn":"AS15133 Edgecast Inc.","city":"Dźwirzyno","region":"West Pomerania","country":"PL","proxy":true}
51+
```
52+
- `./ipscope.bin -url example.org -sub subdomains.txt -dns 8.8.8.8 -json -o output.txt`
53+
```
54+
_
55+
____ _ _ ____| | ___ ____ _____
56+
/ ___) | | |/ ___) |/ _ \| _ \| ___ |
57+
( (___| |_| ( (___| | |_| | | | | ____|
58+
\____)\__ |\____)\_)___/|_| |_|_____)
59+
(____/
60+
61+
Cyclone's IPScope v0.2.4; 2025-01-08
62+
https://github.com/cyclone-github/ipscope
63+
64+
Processing URL: example.org using DNS: 8.8.8.8
3465
35-
The `-dns` flag is useful for testing how a domain resolves with specific DNS servers, such as 1.1.1.1, 8.8.8.8, or DNS based filtering such as Cloudflare 1.1.1.3 or OpenDNS 208.67.222.222. It’s also great for testing locally hosted DNS servers like Pi-hole or pfSense.
66+
Output redirected to file: output.txt
67+
```
68+
### Supported flags:
69+
- `-url {foobar.com} (url to scan)`
70+
- `-sub {subdirectory_file} (defaults to built-in list)`
71+
- `-dns {dns_server} (defaults to 1.1.1.1)`
72+
- `-json (outputs stdout to json format)`
73+
- `-o {output_file} (redirects stdout to file)`
74+
- `-help (prints usage instructions)`
75+
- `-version (prints version info)`
76+
77+
The `-dns` flag is useful for testing how a domain resolves with a specific DNS server, such as 1.1.1.1, 8.8.8.8, or DNS based filtering such as Cloudflare 1.1.1.3 or OpenDNS 208.67.222.222. It’s also great for testing locally hosted DNS servers like Pi-hole or pfSense.
3678

3779
The tool can also be used with a custom subdomain list via the `-sub` flag to verify if known subdomains are resolving correctly through services like Cloudflare, or to check if they are leaking their host IP.
3880

81+
The `-json` flag will format stdout to json, while the `-o` flag will redirect stdout to file.
82+
3983
If neither the `-dns` nor `-sub` flags are given, the tool defaults to 1.1.1.1 and a built-in list of the top 10k common subdomains.
4084

41-
- Supported flags:
42-
- `-url example.org (required)`
43-
- `-sub subdomain.txt (optional, defaults to built-in list)`
44-
- `-dns 8.8.8.8 (optional, defaults to 1.1.1.1)`
45-
- `-help (usage instructions)`
46-
- `-version (version info)`
85+
IPScope output is:
86+
`label` `domain` `ip` `asn` `city` `region` `country` `proxy`
4787

4888
### Compile from source:
4989
- If you want the latest features, compiling from source is the best option since the release version may run several revisions behind the source code.
@@ -55,7 +95,7 @@ If neither the `-dns` nor `-sub` flags are given, the tool defaults to 1.1.1.1 a
5595
- `go build -ldflags="-s -w" .`
5696
- Compile from source code how-to:
5797
- https://github.com/cyclone-github/scripts/blob/main/intro_to_go.txt
58-
### Change Log:
98+
### Changelog:
5999
- https://github.com/cyclone-github/ipscope/blob/main/CHANGELOG.md
60100

61101
### Antivirus False Positives:

main.go

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import (
1313
)
1414

1515
/*
16-
IPScope is a CLI tool for IP lookup and subdomain discovery.
16+
IPScope is a CLI tool for subdomain discovery and IP lookup.
1717
Designed for security researchers and network administrators to resolve IP addresses for TLDs and subdomains.
1818
Includes support for some reverse proxy and WAF detection.
1919
@@ -40,6 +40,10 @@ Version History:
4040
add sanity check for punycode domains, https://github.com/cyclone-github/ipscope/issues/1
4141
add -json output flag, https://github.com/cyclone-github/ipscope/issues/2
4242
fixed stdout to stderr, https://github.com/cyclone-github/ipscope/issues/3
43+
0.2.4; 2025-01-08
44+
added -o {output_file} flag to redirect stdout to file
45+
updated -help output
46+
refactored code
4347
*/
4448

4549
const cloudflareIPv4URL = "https://www.cloudflare.com/ips-v4/"
@@ -57,6 +61,7 @@ func main() {
5761
subFlag := flag.String("sub", "", "File containing subdomains")
5862
dnsFlag := flag.String("dns", "1.1.1.1", "Custom DNS server (ex: 1.1.1.1)")
5963
jsonFlag := flag.Bool("json", false, "Output results in JSON format")
64+
outputFlag := flag.String("o", "", "Output to file (defaults to stdout)")
6065
cycloneFlag := flag.Bool("cyclone", false, "")
6166
versionFlag := flag.Bool("version", false, "Version info")
6267
helpFlag := flag.Bool("help", false, "Display help")
@@ -84,6 +89,16 @@ func main() {
8489
os.Exit(1)
8590
}
8691

92+
if *outputFlag != "" {
93+
file, err := os.OpenFile(*outputFlag, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
94+
if err != nil {
95+
fmt.Fprintf(os.Stderr, "Error opening output file: %v\n", err)
96+
os.Exit(1)
97+
}
98+
defer file.Close()
99+
os.Stdout = file
100+
}
101+
87102
jsonOutput := *jsonFlag
88103

89104
domain := *urlFlag
@@ -107,7 +122,10 @@ func main() {
107122
printCyclone()
108123

109124
fmt.Fprintf(os.Stderr, "Processing URL: %s using DNS: %s\n\n", domain, *dnsFlag)
110-
//writer.Flush()
125+
126+
if *outputFlag != "" {
127+
fmt.Fprintf(os.Stderr, "Output redirected to file: %s\n\n", *outputFlag)
128+
}
111129

112130
// load Cloudflare IP ranges
113131
loadCloudflareIPs()
@@ -121,7 +139,6 @@ func main() {
121139
var err error
122140

123141
for i := 0; i < retries; i++ {
124-
writer.Flush()
125142
crtSubdomains, err = getSubdomainsFromCRT(domain)
126143
if err == nil {
127144
break
@@ -153,7 +170,7 @@ func main() {
153170
if !processedSubdomains[domain] {
154171
tldIPs, err := customResolver.LookupIP(context.Background(), "ip4", domain)
155172
if err != nil {
156-
fmt.Fprintf(writer, "Error getting IP for TLD (%s): %v\n", domain, err)
173+
fmt.Fprintf(os.Stderr, "Error getting IP for TLD (%s): %v\n", domain, err)
157174
} else {
158175
printOutput(writer, "TLD", domain, tldIPs, jsonOutput)
159176
writer.Flush()
@@ -164,7 +181,7 @@ func main() {
164181
if *subFlag != "" {
165182
file, err := os.Open(*subFlag)
166183
if err != nil {
167-
fmt.Fprintf(writer, "Error opening subdomains file (%s): %v\n", *subFlag, err)
184+
fmt.Fprintf(os.Stderr, "Error opening subdomains file (%s): %v\n", *subFlag, err)
168185
os.Exit(1)
169186
}
170187
defer file.Close()
@@ -184,7 +201,7 @@ func main() {
184201
}
185202

186203
if err := scanner.Err(); err != nil {
187-
fmt.Fprintf(writer, "Error reading subdomains file (%s): %v\n", *subFlag, err)
204+
fmt.Fprintf(os.Stderr, "Error reading subdomains file (%s): %v\n", *subFlag, err)
188205
}
189206
} else {
190207
for _, subdomain := range defaultSubdomains() {
@@ -199,4 +216,4 @@ func main() {
199216
}
200217
}
201218
}
202-
}
219+
}

utils.go

Lines changed: 47 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ func init() {
4040
for _, cidr := range staticIPs {
4141
_, ipnet, err := net.ParseCIDR(cidr)
4242
if err != nil {
43-
fmt.Printf("Failed to parse Cloudflare IP range %s: %v\n", cidr, err)
43+
fmt.Fprintf(os.Stderr, "Failed to parse Cloudflare IP range %s: %v\n", cidr, err)
4444
continue
4545
}
4646
cloudflareIPNets = append(cloudflareIPNets, ipnet)
@@ -60,54 +60,40 @@ func printOutput(writer *tabwriter.Writer, label, domain string, ips []net.IP, j
6060
Proxy bool `json:"proxy"`
6161
}
6262

63-
if jsonOutput {
64-
// JSON output format
65-
for _, ip := range ips {
66-
if ipv4 := ip.To4(); ipv4 != nil {
67-
if !isValidPublicIPv4(ipv4) {
68-
continue
69-
}
70-
71-
ipInfo, err := getIPInfo(ipv4.String())
72-
if err != nil {
73-
continue
74-
}
75-
76-
output := JSONOutput{
77-
Label: label,
78-
Domain: domain,
79-
IP: ipv4.String(),
80-
Asn: ipInfo.Org,
81-
City: ipInfo.City,
82-
Region: ipInfo.Region,
83-
Country: ipInfo.Country,
84-
Proxy: checkCloudFlare(ipv4.String()) || checkKnownWAF(ipInfo.Org),
85-
}
86-
87-
jsonData, _ := json.Marshal(output)
88-
fmt.Println(string(jsonData))
89-
}
63+
for _, ip := range ips {
64+
ipv4 := ip.To4()
65+
if ipv4 == nil || !isValidPublicIPv4(ipv4) {
66+
continue
9067
}
91-
} else {
92-
// tabwriter "pretty" output
93-
for _, ip := range ips {
94-
if ipv4 := ip.To4(); ipv4 != nil {
95-
if !isValidPublicIPv4(ipv4) {
96-
continue
97-
}
9868

99-
ipInfo, err := getIPInfo(ipv4.String())
100-
if err != nil {
101-
fmt.Fprintf(writer, "Error fetching IP info: %v\n", err)
102-
continue
103-
}
69+
ipInfo, err := getIPInfo(ipv4.String())
70+
if err != nil {
71+
fmt.Fprintf(os.Stderr, "Error fetching IP info for %s: %v\n", ipv4.String(), err)
72+
continue
73+
}
10474

105-
isReverseProxy := checkCloudFlare(ipv4.String()) || checkKnownWAF(ipInfo.Org)
106-
if isReverseProxy {
107-
fmt.Fprintf(writer, "%-3s\t%-25s\t%-16s\t%-32s\t %s, %s, %s (Reverse Proxy or WAF Detected)\n", label, domain, ipv4, ipInfo.Org, ipInfo.City, ipInfo.Region, ipInfo.Country)
108-
} else {
109-
fmt.Fprintf(writer, "%-3s\t%-25s\t%-16s\t%-32s\t %s, %s, %s\n", label, domain, ipv4, ipInfo.Org, ipInfo.City, ipInfo.Region, ipInfo.Country)
110-
}
75+
isReverseProxy := checkCloudFlare(ipv4.String()) || checkKnownWAF(ipInfo.Org)
76+
77+
if jsonOutput {
78+
// JSON output format
79+
output := JSONOutput{
80+
Label: label,
81+
Domain: domain,
82+
IP: ipv4.String(),
83+
Asn: ipInfo.Org,
84+
City: ipInfo.City,
85+
Region: ipInfo.Region,
86+
Country: ipInfo.Country,
87+
Proxy: isReverseProxy,
88+
}
89+
jsonData, _ := json.Marshal(output)
90+
fmt.Println(string(jsonData))
91+
} else {
92+
// tabwriter "pretty" output
93+
if isReverseProxy {
94+
fmt.Fprintf(writer, "%-3s\t%-25s\t%-16s\t%-32s\t %s, %s, %s (Reverse Proxy or WAF Detected)\n", label, domain, ipv4, ipInfo.Org, ipInfo.City, ipInfo.Region, ipInfo.Country)
95+
} else {
96+
fmt.Fprintf(writer, "%-3s\t%-25s\t%-16s\t%-32s\t %s, %s, %s\n", label, domain, ipv4, ipInfo.Org, ipInfo.City, ipInfo.Region, ipInfo.Country)
11197
}
11298
}
11399
}
@@ -178,7 +164,7 @@ func getIPInfo(ip string) (*IPInfo, error) {
178164
if retryAfter != "" {
179165
if seconds, err := strconv.Atoi(retryAfter); err == nil {
180166
waitTime = time.Duration(seconds) * time.Second
181-
fmt.Printf("Rate-limited: Retrying after %s...\n", waitTime)
167+
fmt.Fprintf(os.Stderr, "Rate-limited: Retrying after %s...\n", waitTime)
182168
}
183169
}
184170

@@ -217,14 +203,14 @@ func checkCloudFlare(ipStr string) bool {
217203
func loadCloudflareIPs() {
218204
resp, err := http.Get(cloudflareIPv4URL)
219205
if err != nil {
220-
fmt.Println("Failed to download Cloudflare IPs, using static list.")
206+
fmt.Fprintln(os.Stderr, "Failed to download Cloudflare IPs, using static list.")
221207
return
222208
}
223209
defer resp.Body.Close()
224210

225211
body, err := io.ReadAll(resp.Body)
226212
if err != nil {
227-
fmt.Println("Failed to read Cloudflare IPs, using static list.")
213+
fmt.Fprintln(os.Stderr, "Failed to read Cloudflare IPs, using static list.")
228214
return
229215
}
230216

@@ -236,7 +222,7 @@ func loadCloudflareIPs() {
236222
}
237223
_, ipnet, err := net.ParseCIDR(ipStr)
238224
if err != nil {
239-
fmt.Printf("Failed to parse Cloudflare IP range %s: %v\n", ipStr, err)
225+
fmt.Fprintf(os.Stderr, "Failed to parse Cloudflare IP range %s: %v\n", ipStr, err)
240226
continue
241227
}
242228
cloudflareIPNets = append(cloudflareIPNets, ipnet)
@@ -266,7 +252,7 @@ func isValidPublicIPv4(ip net.IP) bool {
266252

267253
// version info
268254
func versionFunc() {
269-
fmt.Fprintln(os.Stderr, "Cyclone's IPScope v0.2.3; 2025-01-07\nhttps://github.com/cyclone-github/ipscope\n")
255+
fmt.Fprint(os.Stderr, "Cyclone's IPScope v0.2.4; 2025-01-08\nhttps://github.com/cyclone-github/ipscope\n\n")
270256
}
271257

272258
// cyclone
@@ -280,6 +266,7 @@ func printCyclone() {
280266
(____/
281267
`
282268
fmt.Fprintln(os.Stderr, cyclone)
269+
versionFunc()
283270
time.Sleep(250 * time.Millisecond)
284271
}
285272

@@ -289,15 +276,16 @@ func helpFunc() {
289276
str := `Example Usage:
290277
291278
./ipscope.bin -url example.com
292-
./ipscope.bin -url example.com -sub subdomains.txt -dns 8.8.8.8
279+
./ipscope.bin -url example.com -sub subdomains.txt -dns 8.8.8.8 -json -o output.txt
293280
294281
Supported flags:
295282
296-
-url example.com (required)
297-
-sub subdomain.txt (optional, defaults to built-in list)
298-
-dns 8.8.8.8 (optional, defaults to 1.1.1.1)
299-
300-
-help (usage instructions)
301-
-version (version info)`
283+
-url (url to scan)
284+
-sub (defaults to built-in list)
285+
-dns (defaults to 1.1.1.1)
286+
-json (outputs stdout to json)
287+
-o (redirects stdout to file)
288+
-help (usage instructions)
289+
-version (version info)`
302290
fmt.Fprintln(os.Stderr, str)
303-
}
291+
}

0 commit comments

Comments
 (0)