├── book
├── .gitignore
├── src
│ ├── user-guide
│ │ ├── commands
│ │ │ ├── README.md
│ │ │ └── brute.md
│ │ ├── quickstart
│ │ │ ├── README.md
│ │ │ ├── usage
│ │ │ │ ├── README.md
│ │ │ │ ├── crate.md
│ │ │ │ └── docker.md
│ │ │ └── install.md
│ │ ├── README.md
│ │ └── environments.md
│ ├── development
│ │ ├── README.md
│ │ ├── components
│ │ │ ├── README.md
│ │ │ └── extractors.md
│ │ ├── generics
│ │ │ ├── engine.md
│ │ │ └── integration.md
│ │ └── environment.md
│ ├── SUMMARY.md
│ └── README.md
└── book.toml
├── .hadolint.yaml
├── tests
├── components
│ ├── types
│ │ ├── mod.rs
│ │ └── result
│ │ │ └── mod.rs
│ ├── pools
│ │ └── mod.rs
│ ├── utilities
│ │ └── mod.rs
│ ├── extractors
│ │ ├── mod.rs
│ │ ├── regex_test.rs
│ │ ├── html_test.rs
│ │ └── json_test.rs
│ ├── requesters
│ │ └── mod.rs
│ ├── cli
│ │ ├── mod.rs
│ │ ├── commands
│ │ │ ├── module
│ │ │ │ ├── mod.rs
│ │ │ │ ├── list_test.rs
│ │ │ │ └── get_test.rs
│ │ │ └── mod.rs
│ │ └── cli_test.rs
│ ├── modules
│ │ ├── engines
│ │ │ ├── mod.rs
│ │ │ ├── bing_test.rs
│ │ │ ├── yahoo_test.rs
│ │ │ ├── google_test.rs
│ │ │ └── duckduckgo_test.rs
│ │ ├── generics
│ │ │ └── mod.rs
│ │ ├── integrations
│ │ │ ├── mod.rs
│ │ │ ├── dnsrepo_test.rs
│ │ │ ├── digitorus_test.rs
│ │ │ ├── sitedossier_test.rs
│ │ │ ├── hackertarget_test.rs
│ │ │ ├── waybackarchive_test.rs
│ │ │ ├── crtsh_test.rs
│ │ │ ├── anubis_test.rs
│ │ │ ├── leakix_test.rs
│ │ │ ├── alienvault_test.rs
│ │ │ ├── threatcrowd_test.rs
│ │ │ ├── subdomaincenter_test.rs
│ │ │ ├── chaos_test.rs
│ │ │ ├── bevigil_test.rs
│ │ │ ├── netcraft_test.rs
│ │ │ ├── bufferover_test.rs
│ │ │ ├── whoisxmlapi_test.rs
│ │ │ ├── dnsdumpsterapi_test.rs
│ │ │ ├── securitytrails_test.rs
│ │ │ ├── censys_test.rs
│ │ │ ├── certspotter_test.rs
│ │ │ └── binaryedge_test.rs
│ │ └── mod.rs
│ ├── common
│ │ ├── mod.rs
│ │ ├── mock
│ │ │ ├── mod.rs
│ │ │ ├── resolver.rs
│ │ │ └── modules.rs
│ │ └── constants.rs
│ ├── main.rs
│ └── resolver_test.rs
├── stubs
│ ├── hello
│ │ ├── hello.json
│ │ ├── hello-delayed.json
│ │ ├── hello-with-basic-http-auth.json
│ │ └── hello-with-headers.json
│ └── module
│ │ ├── integrations
│ │ ├── hackertarget.json
│ │ ├── sitedossier.json
│ │ ├── anubis.json
│ │ ├── dnsdumpstercrawler
│ │ │ ├── dnsdumpstercrawler-no-token.json
│ │ │ ├── dnsdumpstercrawler-delayed.json
│ │ │ └── dnsdumpstercrawler-with-token.json
│ │ ├── subdomaincenter.json
│ │ ├── dnsrepo.json
│ │ ├── netcraft.json
│ │ ├── digitorus.json
│ │ ├── threatcrowd.json
│ │ ├── commoncrawl
│ │ │ ├── commoncrawl-index-no-data.json
│ │ │ ├── commoncrawl-index-delayed.json
│ │ │ ├── commoncrawl-cdx-4.json
│ │ │ ├── commoncrawl-cdx-1.json
│ │ │ ├── commoncrawl-cdx-2.json
│ │ │ ├── commoncrawl-cdx-3.json
│ │ │ └── commoncrawl-index-template.json
│ │ ├── alienvault.json
│ │ ├── leakix.json
│ │ ├── github
│ │ │ ├── github-code-search-results.json
│ │ │ ├── github-code-search-no-data.json
│ │ │ ├── github-code-search-delayed.json
│ │ │ └── github-code-search-template.json
│ │ ├── netlas
│ │ │ ├── netlas-no-count.json
│ │ │ ├── netlas-delayed.json
│ │ │ └── with-count
│ │ │ │ ├── netlas-count.json
│ │ │ │ └── netlas-domains-download.json
│ │ ├── bevigil.json
│ │ ├── securitytrails.json
│ │ ├── chaos.json
│ │ ├── dnsdumpsterapi.json
│ │ ├── binaryedge.json
│ │ ├── bufferover.json
│ │ ├── zoomeye.json
│ │ ├── virustotal.json
│ │ ├── builtwith.json
│ │ ├── whoisxmlapi.json
│ │ ├── waybackarchive
│ │ │ ├── waybackarchive.json
│ │ │ └── waybackarchive-delayed.json
│ │ ├── crtsh.json
│ │ ├── certspotter.json
│ │ ├── censys.json
│ │ └── shodan.json
│ │ ├── generics
│ │ ├── integration-with-no-auth.json
│ │ ├── integration-with-invalid-data.json
│ │ ├── integration-with-no-auth-delayed.json
│ │ ├── search-engine.json
│ │ ├── integration-with-basic-http-auth.json
│ │ ├── integration-with-header-auth.json
│ │ ├── integration-with-query-auth.json
│ │ └── search-engine-delayed.json
│ │ └── engines
│ │ ├── bing.json
│ │ ├── google.json
│ │ ├── duckduckgo.json
│ │ └── yahoo.json
└── cache_test.rs
├── .github
├── FUNDING.yml
├── workflows
│ ├── release-plz.yml
│ ├── tests.yml
│ ├── mdbook.yml
│ ├── linters.yml
│ ├── security.yml
│ └── coverage.yml
├── CONTRIBUTING.md
└── dependabot.yml
├── testing
└── testdata
│ ├── txt
│ ├── wordlist.txt
│ └── resolverlist.txt
│ ├── html
│ ├── subdomains.html
│ └── subdomains-with-removes.html
│ └── json
│ └── subdomains.json
├── .codespellrc
├── .mdlrc
├── commitlint.config.js
├── assets
├── logo-dark.png
└── logo-light.png
├── _typos.toml
├── src
├── modules
│ ├── generics
│ │ └── mod.rs
│ ├── mod.rs
│ ├── engines
│ │ ├── mod.rs
│ │ ├── bing.rs
│ │ ├── google.rs
│ │ └── yahoo.rs
│ └── integrations
│ │ └── mod.rs
├── requesters
│ └── mod.rs
├── pools
│ └── mod.rs
├── extractors
│ └── mod.rs
├── interfaces
│ ├── mod.rs
│ ├── lookup.rs
│ ├── extractor.rs
│ └── requester.rs
├── types
│ ├── config
│ │ ├── mod.rs
│ │ └── pool.rs
│ ├── result
│ │ ├── output.rs
│ │ ├── mod.rs
│ │ ├── pool.rs
│ │ ├── metadata.rs
│ │ └── statistics.rs
│ ├── mod.rs
│ ├── core.rs
│ ├── func.rs
│ └── filters.rs
├── enums
│ ├── mod.rs
│ ├── cache.rs
│ ├── output.rs
│ ├── result.rs
│ └── auth.rs
├── utilities
│ ├── mod.rs
│ ├── serializers.rs
│ ├── env.rs
│ ├── regex.rs
│ ├── http.rs
│ └── cli.rs
├── cli
│ ├── commands
│ │ ├── module
│ │ │ ├── list.rs
│ │ │ ├── get.rs
│ │ │ ├── mod.rs
│ │ │ └── run.rs
│ │ ├── mod.rs
│ │ └── brute.rs
│ ├── banner.rs
│ └── mod.rs
├── logger.rs
├── bin
│ └── subscan.rs
└── constants.rs
├── resolverlist.template
├── rustfmt.toml
├── .gitignore
├── codecov.yml
├── .release-plz.toml
├── .env.template
├── examples
├── crate_run_usage.rs
├── custom_extractor.rs
├── crate_brute_usage.rs
├── custom_requester.rs
├── crate_scan_usage.rs
└── custom_module.rs
├── dist-workspace.toml
├── Dockerfile
├── .dockerignore
├── LICENSE
├── deny.toml
└── Cargo.toml
/book/.gitignore:
--------------------------------------------------------------------------------
1 | book
2 |
--------------------------------------------------------------------------------
/.hadolint.yaml:
--------------------------------------------------------------------------------
1 | ignored:
2 | - DL3008
3 |
--------------------------------------------------------------------------------
/tests/components/types/mod.rs:
--------------------------------------------------------------------------------
1 | mod result;
2 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | buy_me_a_coffee: eredotpkfr
2 |
--------------------------------------------------------------------------------
/testing/testdata/txt/wordlist.txt:
--------------------------------------------------------------------------------
1 | foo
2 | bar
3 | baz
4 |
--------------------------------------------------------------------------------
/.codespellrc:
--------------------------------------------------------------------------------
1 | [codespell]
2 | ignore-words-list = crate,ser,statics
3 |
--------------------------------------------------------------------------------
/.mdlrc:
--------------------------------------------------------------------------------
1 | rules "~MD033", "~MD013", "~MD002", "~MD007", "~MD034", "~MD029"
2 |
--------------------------------------------------------------------------------
/tests/components/pools/mod.rs:
--------------------------------------------------------------------------------
1 | automod::dir!("tests/components/pools");
2 |
--------------------------------------------------------------------------------
/tests/components/utilities/mod.rs:
--------------------------------------------------------------------------------
1 | automod::dir!("tests/components/utilities");
2 |
--------------------------------------------------------------------------------
/tests/components/extractors/mod.rs:
--------------------------------------------------------------------------------
1 | automod::dir!("tests/components/extractors");
2 |
--------------------------------------------------------------------------------
/tests/components/requesters/mod.rs:
--------------------------------------------------------------------------------
1 | automod::dir!("tests/components/requesters");
2 |
--------------------------------------------------------------------------------
/tests/components/types/result/mod.rs:
--------------------------------------------------------------------------------
1 | automod::dir!("tests/components/types/result");
2 |
--------------------------------------------------------------------------------
/commitlint.config.js:
--------------------------------------------------------------------------------
1 | export default { extends: ['@commitlint/config-conventional'] };
2 |
--------------------------------------------------------------------------------
/tests/components/cli/mod.rs:
--------------------------------------------------------------------------------
1 | mod commands;
2 |
3 | automod::dir!("tests/components/cli");
4 |
--------------------------------------------------------------------------------
/assets/logo-dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eredotpkfr/subscan/HEAD/assets/logo-dark.png
--------------------------------------------------------------------------------
/assets/logo-light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/eredotpkfr/subscan/HEAD/assets/logo-light.png
--------------------------------------------------------------------------------
/tests/components/modules/engines/mod.rs:
--------------------------------------------------------------------------------
1 | automod::dir!("tests/components/modules/engines");
2 |
--------------------------------------------------------------------------------
/tests/components/modules/generics/mod.rs:
--------------------------------------------------------------------------------
1 | automod::dir!("tests/components/modules/generics");
2 |
--------------------------------------------------------------------------------
/tests/components/cli/commands/module/mod.rs:
--------------------------------------------------------------------------------
1 | automod::dir!("tests/components/cli/commands/module");
2 |
--------------------------------------------------------------------------------
/_typos.toml:
--------------------------------------------------------------------------------
1 | [files]
2 | extend-exclude = ["Cargo.lock", "cliff.toml", ".codespellrc", "CHANGELOG.md"]
3 |
--------------------------------------------------------------------------------
/tests/components/common/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod constants;
2 | pub mod mock;
3 | pub mod stub;
4 | pub mod utils;
5 |
--------------------------------------------------------------------------------
/tests/components/modules/integrations/mod.rs:
--------------------------------------------------------------------------------
1 | automod::dir!("tests/components/modules/integrations");
2 |
--------------------------------------------------------------------------------
/tests/components/cli/commands/mod.rs:
--------------------------------------------------------------------------------
1 | mod module;
2 |
3 | automod::dir!("tests/components/cli/commands");
4 |
--------------------------------------------------------------------------------
/tests/components/common/mock/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod dns;
2 | pub mod funcs;
3 | pub mod modules;
4 | pub mod resolver;
5 |
--------------------------------------------------------------------------------
/tests/components/modules/mod.rs:
--------------------------------------------------------------------------------
1 | mod engines;
2 | mod generics;
3 | mod integrations;
4 |
5 | automod::dir!("tests/components/modules");
6 |
--------------------------------------------------------------------------------
/src/modules/generics/mod.rs:
--------------------------------------------------------------------------------
1 | /// Generic search engine module
2 | pub mod engine;
3 | /// Generic API integration module
4 | pub mod integration;
5 |
--------------------------------------------------------------------------------
/src/requesters/mod.rs:
--------------------------------------------------------------------------------
1 | /// Chrome browser to send HTTP requests via Chrome
2 | pub mod chrome;
3 | /// Basic HTTP client, uses [`reqwest`]
4 | pub mod client;
5 |
--------------------------------------------------------------------------------
/resolverlist.template:
--------------------------------------------------------------------------------
1 | 127.0.0.1:53
2 | 192.168.1.43:8080
3 | 10.126.125.98:4444
4 | [::1]:53
5 | [2001:db8::1]:8080
6 | [2001:db8:85a3:8d3:1319:8a2e:370:7348]:4444
7 |
--------------------------------------------------------------------------------
/src/pools/mod.rs:
--------------------------------------------------------------------------------
1 | /// [`SubscanBrutePool`](crate::pools::brute::SubscanBrutePool) utilities
2 | pub mod brute;
3 | /// [`SubscanModulePool`](crate::pools::module::SubscanModulePool) utilities
4 | pub mod module;
5 |
--------------------------------------------------------------------------------
/tests/components/main.rs:
--------------------------------------------------------------------------------
1 | mod cli;
2 | mod common;
3 | mod extractors;
4 | mod modules;
5 | mod pools;
6 | mod requesters;
7 | mod types;
8 | mod utilities;
9 |
10 | automod::dir!("tests/components");
11 |
--------------------------------------------------------------------------------
/src/extractors/mod.rs:
--------------------------------------------------------------------------------
1 | /// Subdomain extractor for HTML documents
2 | pub mod html;
3 | /// JSON extractor to extract subdomains from JSON content
4 | pub mod json;
5 | /// Extract subdomains with regex statement
6 | pub mod regex;
7 |
--------------------------------------------------------------------------------
/src/modules/mod.rs:
--------------------------------------------------------------------------------
1 | /// Search engine modules
2 | pub mod engines;
3 | /// Generic module implementations
4 | pub mod generics;
5 | /// Integration modules
6 | pub mod integrations;
7 | /// Check zone transfer
8 | pub mod zonetransfer;
9 |
--------------------------------------------------------------------------------
/src/interfaces/mod.rs:
--------------------------------------------------------------------------------
1 | /// Extractor trait definition
2 | pub mod extractor;
3 | /// IP lookup future
4 | pub mod lookup;
5 | /// `Subscan` module trait definition
6 | pub mod module;
7 | /// HTTP requester trait definition
8 | pub mod requester;
9 |
--------------------------------------------------------------------------------
/src/types/config/mod.rs:
--------------------------------------------------------------------------------
1 | /// Subscan thread pool configurations
2 | pub mod pool;
3 | /// Requester configurations
4 | pub mod requester;
5 | /// DNS resolver configurations
6 | pub mod resolver;
7 | /// Subscan configurations
8 | pub mod subscan;
9 |
--------------------------------------------------------------------------------
/rustfmt.toml:
--------------------------------------------------------------------------------
1 | # Configuration for https://rust-lang.github.io/rustfmt/
2 |
3 | edition = "2024"
4 | max_width = 100
5 | chain_width = 80
6 | use_field_init_shorthand = true
7 | use_try_shorthand = true
8 | unstable_features = true
9 | group_imports = "StdExternalCrate"
10 | imports_granularity = "Crate"
11 |
--------------------------------------------------------------------------------
/tests/stubs/hello/hello.json:
--------------------------------------------------------------------------------
1 | {
2 | "request": {
3 | "method": "GET",
4 | "urlPath": "/hello"
5 | },
6 | "response": {
7 | "body": "hello",
8 | "headers": {
9 | "content-type": "text/html"
10 | },
11 | "status": 200
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/enums/mod.rs:
--------------------------------------------------------------------------------
1 | /// Authentication variants
2 | pub mod auth;
3 | /// Cache related enumerations
4 | pub mod cache;
5 | /// Content types
6 | pub mod content;
7 | /// Dispatcher definitions
8 | pub mod dispatchers;
9 | /// Output format options
10 | pub mod output;
11 | /// Result data types
12 | pub mod result;
13 |
--------------------------------------------------------------------------------
/testing/testdata/html/subdomains.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/modules/engines/mod.rs:
--------------------------------------------------------------------------------
1 | /// Enumerate subdomains on `Bing` by using dorking
2 | pub mod bing;
3 | /// Enumerate subdomains on `DuckDuckGo` by using dorking
4 | pub mod duckduckgo;
5 | /// Enumerate subdomains on `Google` by using dorking
6 | pub mod google;
7 | /// Enumerate subdomains on `Yahoo` by using dorking
8 | pub mod yahoo;
9 |
--------------------------------------------------------------------------------
/src/types/result/output.rs:
--------------------------------------------------------------------------------
1 | use std::fs::File;
2 |
3 | use derive_more::From;
4 |
5 | /// Output file type, it stores file name and object
6 | #[derive(From)]
7 | pub struct OutputFile {
8 | /// File name
9 | pub name: String,
10 | /// File object to write or read something
11 | pub descriptor: File,
12 | }
13 |
--------------------------------------------------------------------------------
/src/utilities/mod.rs:
--------------------------------------------------------------------------------
1 | /// Helpful functions related with CLI
2 | pub mod cli;
3 | /// Environment related utilities
4 | pub mod env;
5 | /// HTTP related utilities
6 | pub mod http;
7 | /// Network utilities
8 | pub mod net;
9 | /// Regex utilities
10 | pub mod regex;
11 | /// JSON serializer utilities
12 | pub mod serializers;
13 |
--------------------------------------------------------------------------------
/testing/testdata/html/subdomains-with-removes.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/tests/stubs/module/integrations/hackertarget.json:
--------------------------------------------------------------------------------
1 | {
2 | "request": {
3 | "method": "GET",
4 | "urlPath": "/hackertarget"
5 | },
6 | "response": {
7 | "body": "bar.foo.com\n",
8 | "headers": {
9 | "content-type": "text/html"
10 | },
11 | "status": 200
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Generated by Cargo
2 | # will have compiled files and executables
3 | debug/
4 | target/
5 |
6 | # These are backup files generated by rustfmt
7 | **/*.rs.bk
8 |
9 | # MSVC Windows builds of rustc generate these, which store debugging information
10 | *.pdb
11 |
12 | # Added by cargo
13 | /target
14 |
15 | # Ignore local .env file
16 | .env
17 |
--------------------------------------------------------------------------------
/tests/stubs/module/integrations/sitedossier.json:
--------------------------------------------------------------------------------
1 | {
2 | "request": {
3 | "method": "GET",
4 | "urlPath": "/sitedossier"
5 | },
6 | "response": {
7 | "body": "- bar.foo.com
",
8 | "headers": {
9 | "content-type": "text/html"
10 | },
11 | "status": 200
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/types/result/mod.rs:
--------------------------------------------------------------------------------
1 | /// Result item definitions
2 | pub mod item;
3 | /// Subscan result metadata type
4 | pub mod metadata;
5 | /// Output file types
6 | pub mod output;
7 | /// Pool result definitions
8 | pub mod pool;
9 | /// Statistics types
10 | pub mod statistics;
11 | /// Status types
12 | pub mod status;
13 | /// Scan result types
14 | pub mod subscan;
15 |
--------------------------------------------------------------------------------
/tests/stubs/hello/hello-delayed.json:
--------------------------------------------------------------------------------
1 | {
2 | "request": {
3 | "method": "GET",
4 | "urlPath": "/hello-delayed"
5 | },
6 | "response": {
7 | "body": "hello",
8 | "fixedDelayMilliseconds": 1000,
9 | "headers": {
10 | "content-type": "text/html"
11 | },
12 | "status": 200
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/tests/stubs/module/integrations/anubis.json:
--------------------------------------------------------------------------------
1 | {
2 | "request": {
3 | "method": "GET",
4 | "urlPath": "/anubis"
5 | },
6 | "response": {
7 | "headers": {
8 | "content-type": "application/json"
9 | },
10 | "jsonBody": [
11 | "bar.foo.com"
12 | ],
13 | "status": 200
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/tests/stubs/module/integrations/dnsdumpstercrawler/dnsdumpstercrawler-no-token.json:
--------------------------------------------------------------------------------
1 | {
2 | "request": {
3 | "method": "GET",
4 | "urlPath": "/dnsdumpstercrawler-no-token"
5 | },
6 | "response": {
7 | "body": "foo",
8 | "headers": {
9 | "content-type": "text/html"
10 | },
11 | "status": 200
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/tests/stubs/module/integrations/subdomaincenter.json:
--------------------------------------------------------------------------------
1 | {
2 | "request": {
3 | "method": "GET",
4 | "urlPath": "/subdomaincenter"
5 | },
6 | "response": {
7 | "headers": {
8 | "content-type": "application/json"
9 | },
10 | "jsonBody": [
11 | "bar.foo.com"
12 | ],
13 | "status": 200
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/tests/stubs/module/integrations/dnsrepo.json:
--------------------------------------------------------------------------------
1 | {
2 | "request": {
3 | "method": "GET",
4 | "urlPath": "/dnsrepo"
5 | },
6 | "response": {
7 | "body": "",
8 | "headers": {
9 | "content-type": "text/html"
10 | },
11 | "status": 200
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/tests/stubs/module/integrations/netcraft.json:
--------------------------------------------------------------------------------
1 | {
2 | "request": {
3 | "method": "GET",
4 | "urlPath": "/netcraft"
5 | },
6 | "response": {
7 | "body": "",
8 | "headers": {
9 | "content-type": "text/html"
10 | },
11 | "status": 200
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/tests/stubs/module/integrations/digitorus.json:
--------------------------------------------------------------------------------
1 | {
2 | "request": {
3 | "method": "GET",
4 | "urlPath": "/digitorus"
5 | },
6 | "response": {
7 | "body": "",
8 | "headers": {
9 | "content-type": "text/html"
10 | },
11 | "status": 200
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/types/mod.rs:
--------------------------------------------------------------------------------
1 | /// Type definitions are related with any configuration
2 | pub mod config;
3 | /// Core type definitions
4 | pub mod core;
5 | /// Types that related environments
6 | pub mod env;
7 | /// Filter definitions
8 | pub mod filters;
9 | /// Function type definitions
10 | pub mod func;
11 | /// Query types for search engines
12 | pub mod query;
13 | /// `Subscan` module result types
14 | pub mod result;
15 |
--------------------------------------------------------------------------------
/tests/stubs/module/integrations/dnsdumpstercrawler/dnsdumpstercrawler-delayed.json:
--------------------------------------------------------------------------------
1 | {
2 | "request": {
3 | "method": "GET",
4 | "urlPath": "/dnsdumpstercrawler-delayed"
5 | },
6 | "response": {
7 | "body": "foo",
8 | "fixedDelayMilliseconds": 1000,
9 | "headers": {
10 | "content-type": "text/html"
11 | },
12 | "status": 200
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/tests/components/common/constants.rs:
--------------------------------------------------------------------------------
1 | pub const LOCAL_HOST: &str = "127.0.0.1";
2 | pub const TEST_URL: &str = "http://foo.com";
3 | pub const TEST_DOMAIN: &str = "foo.com";
4 | pub const TEST_FOO_SUBDOMAIN: &str = "foo.foo.com";
5 | pub const TEST_BAR_SUBDOMAIN: &str = "bar.foo.com";
6 | pub const TEST_BAZ_SUBDOMAIN: &str = "baz.foo.com";
7 | pub const TEST_API_KEY: &str = "test-api-key";
8 | pub const READ_ERROR: &str = "Cannot read file!";
9 |
--------------------------------------------------------------------------------
/tests/stubs/module/integrations/threatcrowd.json:
--------------------------------------------------------------------------------
1 | {
2 | "request": {
3 | "method": "GET",
4 | "urlPath": "/threatcrowd"
5 | },
6 | "response": {
7 | "headers": {
8 | "content-type": "application/json"
9 | },
10 | "jsonBody": {
11 | "subdomains": [
12 | "bar.foo.com"
13 | ]
14 | },
15 | "status": 200
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/tests/stubs/module/generics/integration-with-no-auth.json:
--------------------------------------------------------------------------------
1 | {
2 | "request": {
3 | "method": "GET",
4 | "urlPath": "/subdomains"
5 | },
6 | "response": {
7 | "headers": {
8 | "content-type": "application/json"
9 | },
10 | "jsonBody": {
11 | "subdomains": [
12 | "bar.foo.com"
13 | ]
14 | },
15 | "status": 200
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/tests/stubs/hello/hello-with-basic-http-auth.json:
--------------------------------------------------------------------------------
1 | {
2 | "request": {
3 | "basicAuth": {
4 | "password": "bar",
5 | "username": "foo"
6 | },
7 | "method": "GET",
8 | "urlPath": "/hello-with-basic-http-auth"
9 | },
10 | "response": {
11 | "body": "hello",
12 | "headers": {
13 | "content-type": "text/html"
14 | },
15 | "status": 200
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/tests/stubs/module/integrations/commoncrawl/commoncrawl-index-no-data.json:
--------------------------------------------------------------------------------
1 | {
2 | "request": {
3 | "method": "GET",
4 | "urlPath": "/commoncrawl/index-no-data"
5 | },
6 | "response": {
7 | "headers": {
8 | "content-type": "application/json"
9 | },
10 | "jsonBody": {},
11 | "status": 200,
12 | "transformers": [
13 | "response-template"
14 | ]
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/tests/stubs/module/integrations/dnsdumpstercrawler/dnsdumpstercrawler-with-token.json:
--------------------------------------------------------------------------------
1 | {
2 | "request": {
3 | "method": "ANY",
4 | "urlPath": "/dnsdumpstercrawler-with-token"
5 | },
6 | "response": {
7 | "body": "\"Authorization\": \"foo\" ",
8 | "headers": {
9 | "content-type": "text/html"
10 | },
11 | "status": 200
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/tests/stubs/module/generics/integration-with-invalid-data.json:
--------------------------------------------------------------------------------
1 | {
2 | "request": {
3 | "method": "GET",
4 | "urlPath": "/subdomains"
5 | },
6 | "response": {
7 | "headers": {
8 | "content-type": "application/json"
9 | },
10 | "jsonBody": {
11 | "subdomains": [
12 | "bar.foo.com",
13 | "foo.bar.com"
14 | ]
15 | },
16 | "status": 200
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/tests/stubs/module/generics/integration-with-no-auth-delayed.json:
--------------------------------------------------------------------------------
1 | {
2 | "request": {
3 | "method": "GET",
4 | "urlPath": "/subdomains"
5 | },
6 | "response": {
7 | "fixedDelayMilliseconds": 1000,
8 | "headers": {
9 | "content-type": "application/json"
10 | },
11 | "jsonBody": {
12 | "subdomains": [
13 | "bar.foo.com"
14 | ]
15 | },
16 | "status": 200
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/tests/stubs/module/integrations/alienvault.json:
--------------------------------------------------------------------------------
1 | {
2 | "request": {
3 | "method": "GET",
4 | "urlPath": "/alienvault"
5 | },
6 | "response": {
7 | "headers": {
8 | "content-type": "application/json"
9 | },
10 | "jsonBody": {
11 | "passive_dns": [
12 | {
13 | "hostname": "bar.foo.com"
14 | }
15 | ]
16 | },
17 | "status": 200
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/testing/testdata/json/subdomains.json:
--------------------------------------------------------------------------------
1 | {
2 | "data": {
3 | "subdomains": [
4 | {
5 | "ip": "127.0.0.1",
6 | "subdomain": "bar.foo.com"
7 | },
8 | {
9 | "ip": "127.0.0.1",
10 | "subdomain": "baz.foo.com"
11 | }
12 | ]
13 | },
14 | "id": "7a3db2d9-1713-4e72-8261-5976fa5b9cf9",
15 | "scan_time": "2024-09-22T11:15:00.430218",
16 | "status": "SUCCESS"
17 | }
18 |
--------------------------------------------------------------------------------
/tests/stubs/module/integrations/commoncrawl/commoncrawl-index-delayed.json:
--------------------------------------------------------------------------------
1 | {
2 | "request": {
3 | "method": "GET",
4 | "urlPath": "/commoncrawl/index-delayed"
5 | },
6 | "response": {
7 | "fixedDelayMilliseconds": 1000,
8 | "headers": {
9 | "content-type": "application/json"
10 | },
11 | "jsonBody": {},
12 | "status": 200,
13 | "transformers": [
14 | "response-template"
15 | ]
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/book/src/user-guide/commands/README.md:
--------------------------------------------------------------------------------
1 | # Commands
2 |
3 | This chapter provides a comprehensive guide to the `Subscan` CLI commands. Below is a list of available commands. For detailed information on usage and arguments, refer to the corresponding sections
4 |
5 |
6 |
7 | - [scan](scan.md)
8 | - [brute](brute.md)
9 | - [module](module.md)
10 | - [list](module.md#list)
11 | - [get](module.md#get)
12 | - [run](module.md#run)
13 |
14 |
15 |
--------------------------------------------------------------------------------
/tests/stubs/module/integrations/leakix.json:
--------------------------------------------------------------------------------
1 | {
2 | "request": {
3 | "method": "GET",
4 | "urlPath": "/leakix"
5 | },
6 | "response": {
7 | "headers": {
8 | "content-type": "application/json"
9 | },
10 | "jsonBody": [
11 | {
12 | "distinct_ips": 1,
13 | "last_seen": "2021-05-13T10:28:34.356Z",
14 | "subdomain": "bar.foo.com"
15 | }
16 | ],
17 | "status": 200
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/book/src/user-guide/quickstart/README.md:
--------------------------------------------------------------------------------
1 | # Quickstart
2 |
3 | `Subscan` is a fast and efficient subdomain enumeration tool designed for penetration testers and security researchers. In this chapter, you'll learn how to quickly set up and start using `Subscan` to discover subdomains and improve your security assessments
4 |
5 | Here's what you'll find in this chapter
6 |
7 | - [Installing Subscan](install.md)
8 | - [Usage Methods and Scenarios](usage/index.html)
9 | - [Details of Commands and Typical Use Cases](../commands/index.html)
10 |
--------------------------------------------------------------------------------
/tests/stubs/module/integrations/commoncrawl/commoncrawl-cdx-4.json:
--------------------------------------------------------------------------------
1 | {
2 | "request": {
3 | "method": "GET",
4 | "queryParameters": {
5 | "url": {
6 | "equalTo": "*.foo.com"
7 | }
8 | },
9 | "urlPath": "/commoncrawl/cdx-4"
10 | },
11 | "response": {
12 | "body": "bar.foo.com",
13 | "fixedDelayMilliseconds": 100,
14 | "headers": {
15 | "content-type": "text/html"
16 | },
17 | "status": 200
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/tests/stubs/module/integrations/github/github-code-search-results.json:
--------------------------------------------------------------------------------
1 | {
2 | "request": {
3 | "headers": {
4 | "Authorization": {
5 | "equalTo": "token github-api-key"
6 | }
7 | },
8 | "method": "GET",
9 | "urlPath": "/github-code-search/results"
10 | },
11 | "response": {
12 | "body": "this is bar.foo.com content",
13 | "headers": {
14 | "content-type": "text/html"
15 | },
16 | "status": 200
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/tests/stubs/module/integrations/netlas/netlas-no-count.json:
--------------------------------------------------------------------------------
1 | {
2 | "request": {
3 | "headers": {
4 | "X-API-Key": {
5 | "equalTo": "netlas-api-key"
6 | }
7 | },
8 | "method": "GET",
9 | "urlPath": "/netlas-no-count"
10 | },
11 | "response": {
12 | "headers": {
13 | "content-type": "application/json"
14 | },
15 | "jsonBody": {
16 | "no-count": "foo"
17 | },
18 | "status": 200
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/tests/stubs/module/generics/search-engine.json:
--------------------------------------------------------------------------------
1 | {
2 | "request": {
3 | "method": "GET",
4 | "queryParameters": {
5 | "num": {
6 | "equalTo": 100
7 | },
8 | "q": {
9 | "equalTo": "site:foo.com"
10 | }
11 | },
12 | "urlPath": "/search"
13 | },
14 | "response": {
15 | "body": "bar.foo.com",
16 | "headers": {
17 | "content-type": "text/html"
18 | },
19 | "status": 200
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/tests/stubs/module/engines/bing.json:
--------------------------------------------------------------------------------
1 | {
2 | "request": {
3 | "method": "GET",
4 | "queryParameters": {
5 | "num": {
6 | "equalTo": 100
7 | },
8 | "q": {
9 | "equalTo": "site:foo.com"
10 | }
11 | },
12 | "urlPath": "/search"
13 | },
14 | "response": {
15 | "body": "bar.foo.com
",
16 | "headers": {
17 | "content-type": "text/html"
18 | },
19 | "status": 200
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/tests/stubs/module/engines/google.json:
--------------------------------------------------------------------------------
1 | {
2 | "request": {
3 | "method": "GET",
4 | "queryParameters": {
5 | "num": {
6 | "equalTo": 100
7 | },
8 | "q": {
9 | "equalTo": "site:foo.com"
10 | }
11 | },
12 | "urlPath": "/search"
13 | },
14 | "response": {
15 | "body": "bar.foo.com
",
16 | "headers": {
17 | "content-type": "text/html"
18 | },
19 | "status": 200
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/tests/stubs/module/generics/integration-with-basic-http-auth.json:
--------------------------------------------------------------------------------
1 | {
2 | "request": {
3 | "basicAuth": {
4 | "password": "bar",
5 | "username": "foo"
6 | },
7 | "method": "GET",
8 | "urlPath": "/subdomains"
9 | },
10 | "response": {
11 | "headers": {
12 | "content-type": "application/json"
13 | },
14 | "jsonBody": {
15 | "subdomains": [
16 | "bar.foo.com"
17 | ]
18 | },
19 | "status": 200
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/codecov.yml:
--------------------------------------------------------------------------------
1 | coverage: # https://docs.codecov.com/docs/codecovyml-reference#coverage
2 | precision: 2 # e.g. 91.67%
3 | round: nearest
4 | range: 65..85 # https://docs.codecov.com/docs/coverage-configuration#section-range
5 |
6 | status: # https://docs.codecov.com/docs/commit-status
7 | project:
8 | default:
9 | target: auto
10 | threshold: "1%"
11 | if_ci_failed: error
12 | branches:
13 | - main
14 | patch: off
15 |
16 | ignore:
17 | - "src/lib.rs"
18 | - "src/logger.rs"
19 | - "src/bin/subscan.rs"
20 |
--------------------------------------------------------------------------------
/tests/stubs/module/integrations/bevigil.json:
--------------------------------------------------------------------------------
1 | {
2 | "request": {
3 | "headers": {
4 | "X-Access-Token": {
5 | "equalTo": "bevigil-api-key"
6 | }
7 | },
8 | "method": "GET",
9 | "urlPath": "/bevigil"
10 | },
11 | "response": {
12 | "headers": {
13 | "content-type": "application/json"
14 | },
15 | "jsonBody": {
16 | "subdomains": [
17 | "bar.foo.com"
18 | ]
19 | },
20 | "status": 200
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/tests/components/cli/cli_test.rs:
--------------------------------------------------------------------------------
1 | use clap::Parser;
2 | use subscan::cli::Cli;
3 |
4 | #[tokio::test]
5 | #[should_panic]
6 | async fn cli_parse_error_test() {
7 | Cli::try_parse_from(vec!["subscan", "-x"]).unwrap();
8 | }
9 |
10 | #[tokio::test]
11 | async fn verbosity_test() {
12 | let args = vec!["subscan", "scan", "-d", "foo.com", "-qqqq"];
13 | let cli = Cli::try_parse_from(args).unwrap();
14 |
15 | cli.init().await;
16 | cli.banner().await;
17 |
18 | assert!(cli.verbose.is_present());
19 | assert_eq!(cli.verbose.to_string(), "off");
20 | }
21 |
--------------------------------------------------------------------------------
/tests/stubs/module/generics/integration-with-header-auth.json:
--------------------------------------------------------------------------------
1 | {
2 | "request": {
3 | "headers": {
4 | "X-API-Key": {
5 | "equalTo": "test-api-key"
6 | }
7 | },
8 | "method": "GET",
9 | "urlPath": "/subdomains"
10 | },
11 | "response": {
12 | "headers": {
13 | "content-type": "application/json"
14 | },
15 | "jsonBody": {
16 | "subdomains": [
17 | "bar.foo.com"
18 | ]
19 | },
20 | "status": 200
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/tests/stubs/module/integrations/netlas/netlas-delayed.json:
--------------------------------------------------------------------------------
1 | {
2 | "request": {
3 | "headers": {
4 | "X-API-Key": {
5 | "equalTo": "netlas-api-key"
6 | }
7 | },
8 | "method": "GET",
9 | "urlPath": "/api/domains_count/"
10 | },
11 | "response": {
12 | "fixedDelayMilliseconds": 1000,
13 | "headers": {
14 | "content-type": "application/json"
15 | },
16 | "jsonBody": {
17 | "no-count": "foo"
18 | },
19 | "status": 200
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/tests/stubs/module/integrations/securitytrails.json:
--------------------------------------------------------------------------------
1 | {
2 | "request": {
3 | "headers": {
4 | "APIKEY": {
5 | "equalTo": "securitytrails-api-key"
6 | }
7 | },
8 | "method": "GET",
9 | "urlPath": "/securitytrails"
10 | },
11 | "response": {
12 | "headers": {
13 | "content-type": "application/json"
14 | },
15 | "jsonBody": {
16 | "status": 200,
17 | "subdomains": [
18 | "bar"
19 | ]
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/tests/stubs/module/generics/integration-with-query-auth.json:
--------------------------------------------------------------------------------
1 | {
2 | "request": {
3 | "method": "GET",
4 | "queryParameters": {
5 | "apikey": {
6 | "equalTo": "test-api-key"
7 | }
8 | },
9 | "urlPath": "/subdomains"
10 | },
11 | "response": {
12 | "headers": {
13 | "content-type": "application/json"
14 | },
15 | "jsonBody": {
16 | "subdomains": [
17 | "bar.foo.com"
18 | ]
19 | },
20 | "status": 200
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/tests/stubs/module/engines/duckduckgo.json:
--------------------------------------------------------------------------------
1 | {
2 | "request": {
3 | "method": "GET",
4 | "queryParameters": {
5 | "num": {
6 | "equalTo": 100
7 | },
8 | "q": {
9 | "equalTo": "site:foo.com"
10 | }
11 | }
12 | },
13 | "response": {
14 | "body": "",
15 | "headers": {
16 | "content-type": "text/html"
17 | },
18 | "status": 200
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/tests/stubs/module/generics/search-engine-delayed.json:
--------------------------------------------------------------------------------
1 | {
2 | "request": {
3 | "method": "GET",
4 | "queryParameters": {
5 | "num": {
6 | "equalTo": 100
7 | },
8 | "q": {
9 | "equalTo": "site:foo.com"
10 | }
11 | },
12 | "urlPath": "/search"
13 | },
14 | "response": {
15 | "body": "bar.foo.com",
16 | "fixedDelayMilliseconds": 1000,
17 | "headers": {
18 | "content-type": "text/html"
19 | },
20 | "status": 200
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/tests/stubs/module/integrations/chaos.json:
--------------------------------------------------------------------------------
1 | {
2 | "request": {
3 | "headers": {
4 | "Authorization": {
5 | "equalTo": "chaos-api-key"
6 | }
7 | },
8 | "method": "GET",
9 | "urlPath": "/chaos"
10 | },
11 | "response": {
12 | "headers": {
13 | "content-type": "application/json"
14 | },
15 | "jsonBody": {
16 | "domain": "foo.com",
17 | "subdomains": [
18 | "bar"
19 | ]
20 | },
21 | "status": 200
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/tests/stubs/hello/hello-with-headers.json:
--------------------------------------------------------------------------------
1 | {
2 | "request": {
3 | "headers": {
4 | "x-api-key": {
5 | "equalTo": "hello-api"
6 | }
7 | },
8 | "method": "GET",
9 | "queryParameters": {
10 | "search": {
11 | "equalTo": "site:foo.com"
12 | }
13 | },
14 | "urlPath": "/hello-with-headers"
15 | },
16 | "response": {
17 | "body": "hello",
18 | "headers": {
19 | "content-type": "text/html"
20 | },
21 | "status": 200
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/tests/stubs/module/engines/yahoo.json:
--------------------------------------------------------------------------------
1 | {
2 | "request": {
3 | "method": "GET",
4 | "queryParameters": {
5 | "num": {
6 | "equalTo": 100
7 | },
8 | "p": {
9 | "equalTo": "site:foo.com"
10 | }
11 | },
12 | "urlPath": "/search"
13 | },
14 | "response": {
15 | "body": "
",
16 | "headers": {
17 | "content-type": "text/html"
18 | },
19 | "status": 200
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/tests/stubs/module/integrations/commoncrawl/commoncrawl-cdx-1.json:
--------------------------------------------------------------------------------
1 | {
2 | "request": {
3 | "method": "GET",
4 | "queryParameters": {
5 | "url": {
6 | "equalTo": "*.foo.com"
7 | }
8 | },
9 | "urlPath": "/commoncrawl/cdx-1"
10 | },
11 | "response": {
12 | "body": "bar.foo.com",
13 | "headers": {
14 | "accept-ranges": "bytes",
15 | "connection": "keep-alive",
16 | "content-length": 11,
17 | "content-type": "text/html"
18 | },
19 | "status": 200
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/tests/stubs/module/integrations/commoncrawl/commoncrawl-cdx-2.json:
--------------------------------------------------------------------------------
1 | {
2 | "request": {
3 | "method": "GET",
4 | "queryParameters": {
5 | "url": {
6 | "equalTo": "*.foo.com"
7 | }
8 | },
9 | "urlPath": "/commoncrawl/cdx-2"
10 | },
11 | "response": {
12 | "body": "baz.foo.com",
13 | "headers": {
14 | "accept-ranges": "bytes",
15 | "connection": "keep-alive",
16 | "content-length": 11,
17 | "content-type": "text/html"
18 | },
19 | "status": 200
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/tests/stubs/module/integrations/commoncrawl/commoncrawl-cdx-3.json:
--------------------------------------------------------------------------------
1 | {
2 | "request": {
3 | "method": "GET",
4 | "queryParameters": {
5 | "url": {
6 | "equalTo": "*.foo.com"
7 | }
8 | },
9 | "urlPath": "/commoncrawl/cdx-3"
10 | },
11 | "response": {
12 | "body": "barbaz.foo.com",
13 | "headers": {
14 | "accept-ranges": "bytes",
15 | "connection": "keep-alive",
16 | "content-length": 11,
17 | "content-type": "text/html"
18 | },
19 | "status": 200
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/tests/stubs/module/integrations/dnsdumpsterapi.json:
--------------------------------------------------------------------------------
1 | {
2 | "request": {
3 | "headers": {
4 | "X-API-Key": {
5 | "equalTo": "dnsdumpsterapi-api-key"
6 | }
7 | },
8 | "method": "GET",
9 | "urlPath": "/dnsdumpsterapi"
10 | },
11 | "response": {
12 | "headers": {
13 | "content-type": "application/json"
14 | },
15 | "jsonBody": {
16 | "a": [
17 | {
18 | "host": "bar.foo.com"
19 | }
20 | ]
21 | },
22 | "status": 200
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/cli/commands/module/list.rs:
--------------------------------------------------------------------------------
1 | use std::io::Write;
2 |
3 | use clap::Args;
4 |
5 | use crate::{types::core::SubscanModule, utilities::cli};
6 |
7 | /// List command to list implemented modules
8 | #[derive(Args, Clone, Debug)]
9 | pub struct ModuleListSubCommandArgs {}
10 |
11 | impl ModuleListSubCommandArgs {
12 | pub async fn as_table(&self, modules: &Vec, out: &mut W) {
13 | let mut table = cli::create_module_table().await;
14 |
15 | for module in modules {
16 | table.add_row(module.lock().await.as_table_row().await);
17 | }
18 |
19 | table.print(out).unwrap();
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/book/book.toml:
--------------------------------------------------------------------------------
1 | [book]
2 | authors = ["Erdoğan Yoksul "]
3 | language = "en"
4 | src = "src"
5 | title = "subscan"
6 | description = "A subdomain enumeration tool leveraging diverse techniques, designed for advanced pentesting operations"
7 |
8 | [rust]
9 | edition = "2021"
10 |
11 | [build]
12 | create-missing = false
13 |
14 | [output.html.search]
15 | limit-results = 15
16 |
17 | [output.html]
18 | default-theme = "ayu"
19 | git-repository-url = "https://github.com/eredotpkfr/subscan"
20 | cname = "www.erdoganyoksul.com"
21 |
22 | [output.html.playground]
23 | runnable = false
24 |
25 | [preprocessor.index]
26 |
27 | [preprocessor.links]
28 |
--------------------------------------------------------------------------------
/tests/components/modules/engines/bing_test.rs:
--------------------------------------------------------------------------------
1 | use subscan::{modules::engines::bing::Bing, types::result::status::SubscanModuleStatus};
2 |
3 | use crate::common::{
4 | constants::{TEST_BAR_SUBDOMAIN, TEST_DOMAIN},
5 | mock::funcs,
6 | utils,
7 | };
8 |
9 | #[tokio::test]
10 | #[stubr::mock("module/engines/bing.json")]
11 | async fn run_test() {
12 | let mut bing = Bing::dispatcher();
13 |
14 | funcs::wrap_module_url(&mut bing, &stubr.path("/search"));
15 |
16 | let (results, status) = utils::run_module(bing, TEST_DOMAIN).await;
17 |
18 | assert_eq!(results, [TEST_BAR_SUBDOMAIN.into()].into());
19 | assert_eq!(status, SubscanModuleStatus::Finished);
20 | }
21 |
--------------------------------------------------------------------------------
/tests/components/modules/engines/yahoo_test.rs:
--------------------------------------------------------------------------------
1 | use subscan::{modules::engines::yahoo::Yahoo, types::result::status::SubscanModuleStatus};
2 |
3 | use crate::common::{
4 | constants::{TEST_BAR_SUBDOMAIN, TEST_DOMAIN},
5 | mock::funcs,
6 | utils,
7 | };
8 |
9 | #[tokio::test]
10 | #[stubr::mock("module/engines/yahoo.json")]
11 | async fn run_test() {
12 | let mut yahoo = Yahoo::dispatcher();
13 |
14 | funcs::wrap_module_url(&mut yahoo, &stubr.path("/search"));
15 |
16 | let (results, status) = utils::run_module(yahoo, TEST_DOMAIN).await;
17 |
18 | assert_eq!(results, [TEST_BAR_SUBDOMAIN.into()].into());
19 | assert_eq!(status, SubscanModuleStatus::Finished);
20 | }
21 |
--------------------------------------------------------------------------------
/tests/stubs/module/integrations/binaryedge.json:
--------------------------------------------------------------------------------
1 | {
2 | "request": {
3 | "headers": {
4 | "X-Key": {
5 | "equalTo": "binaryedge-api-key"
6 | }
7 | },
8 | "method": "GET",
9 | "queryParameters": {
10 | "page": {
11 | "absent": true
12 | }
13 | },
14 | "urlPath": "/binaryedge"
15 | },
16 | "response": {
17 | "headers": {
18 | "content-type": "application/json"
19 | },
20 | "jsonBody": {
21 | "events": [
22 | "bar.foo.com"
23 | ]
24 | },
25 | "status": 200
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/tests/stubs/module/integrations/netlas/with-count/netlas-count.json:
--------------------------------------------------------------------------------
1 | {
2 | "request": {
3 | "headers": {
4 | "X-API-Key": {
5 | "equalTo": "netlas-api-key"
6 | }
7 | },
8 | "method": "GET",
9 | "queryParameters": {
10 | "q": {
11 | "equalTo": "domain:*.foo.com AND NOT domain:foo.com"
12 | }
13 | },
14 | "urlPath": "/api/domains_count/"
15 | },
16 | "response": {
17 | "headers": {
18 | "content-type": "application/json"
19 | },
20 | "jsonBody": {
21 | "count": 1
22 | },
23 | "status": 200
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/tests/components/modules/engines/google_test.rs:
--------------------------------------------------------------------------------
1 | use subscan::{modules::engines::google::Google, types::result::status::SubscanModuleStatus};
2 |
3 | use crate::common::{
4 | constants::{TEST_BAR_SUBDOMAIN, TEST_DOMAIN},
5 | mock::funcs,
6 | utils,
7 | };
8 |
9 | #[tokio::test]
10 | #[stubr::mock("module/engines/google.json")]
11 | async fn run_test() {
12 | let mut google = Google::dispatcher();
13 |
14 | funcs::wrap_module_url(&mut google, &stubr.path("/search"));
15 |
16 | let (results, status) = utils::run_module(google, TEST_DOMAIN).await;
17 |
18 | assert_eq!(results, [TEST_BAR_SUBDOMAIN.into()].into());
19 | assert_eq!(status, SubscanModuleStatus::Finished);
20 | }
21 |
--------------------------------------------------------------------------------
/.release-plz.toml:
--------------------------------------------------------------------------------
1 | [workspace]
2 | # path of the git-cliff configuration
3 | changelog_config = "cliff.toml"
4 |
5 | # enable changelog updates
6 | changelog_update = true
7 |
8 | # update dependencies with `cargo update`
9 | dependencies_update = true
10 |
11 | # create tags for the releases
12 | git_tag_enable = true
13 |
14 | # disable GitHub releases
15 | git_release_enable = false
16 |
17 | # labels for the release PR
18 | pr_labels = ["release"]
19 |
20 | # disallow updating repositories with uncommitted changes
21 | allow_dirty = false
22 |
23 | # disallow packaging with uncommitted changes
24 | publish_allow_dirty = false
25 |
26 | # disable running `cargo-semver-checks`
27 | semver_check = false
28 |
--------------------------------------------------------------------------------
/src/cli/commands/module/get.rs:
--------------------------------------------------------------------------------
1 | use std::io::Write;
2 |
3 | use clap::Args;
4 | use tokio::sync::Mutex;
5 |
6 | use crate::{enums::dispatchers::SubscanModuleDispatcher, utilities::cli};
7 |
8 | /// Get command to fetch any module
9 | #[derive(Args, Clone, Debug)]
10 | pub struct ModuleGetSubCommandArgs {
11 | /// Module name to be fetched
12 | pub name: String,
13 | }
14 |
15 | impl ModuleGetSubCommandArgs {
16 | pub async fn as_table(&self, module: &Mutex, out: &mut W) {
17 | let mut table = cli::create_module_table().await;
18 |
19 | table.add_row(module.lock().await.as_table_row().await);
20 | table.print(out).unwrap();
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/tests/stubs/module/integrations/bufferover.json:
--------------------------------------------------------------------------------
1 | {
2 | "request": {
3 | "headers": {
4 | "X-API-Key": {
5 | "equalTo": "bufferover-api-key"
6 | }
7 | },
8 | "method": "GET",
9 | "urlPath": "/bufferover"
10 | },
11 | "response": {
12 | "headers": {
13 | "content-type": "application/json"
14 | },
15 | "jsonBody": {
16 | "Meta": {
17 | "domain": "foo.com"
18 | },
19 | "Results": [
20 | "54.201.204.183,581faa6ff692d0ba8185753570c8624a2a6b4e8e47bd5322216cc12a41def044,,bar.foo.com"
21 | ]
22 | },
23 | "status": 200
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/tests/stubs/module/integrations/zoomeye.json:
--------------------------------------------------------------------------------
1 | {
2 | "request": {
3 | "headers": {
4 | "API-Key": {
5 | "equalTo": "zoomeye-api-key"
6 | }
7 | },
8 | "method": "GET",
9 | "queryParameters": {
10 | "page": {
11 | "absent": true
12 | }
13 | },
14 | "urlPath": "/zoomeye"
15 | },
16 | "response": {
17 | "headers": {
18 | "content-type": "application/json"
19 | },
20 | "jsonBody": {
21 | "list": [
22 | {
23 | "name": "bar.foo.com"
24 | }
25 | ]
26 | },
27 | "status": 200
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/tests/stubs/module/integrations/virustotal.json:
--------------------------------------------------------------------------------
1 | {
2 | "request": {
3 | "headers": {
4 | "X-APIKey": {
5 | "equalTo": "virustotal-api-key"
6 | }
7 | },
8 | "method": "GET",
9 | "queryParameters": {
10 | "cursor": {
11 | "absent": true
12 | }
13 | },
14 | "urlPath": "/virustotal"
15 | },
16 | "response": {
17 | "headers": {
18 | "content-type": "application/json"
19 | },
20 | "jsonBody": {
21 | "data": [
22 | {
23 | "id": "bar.foo.com"
24 | }
25 | ]
26 | },
27 | "status": 200
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/.env.template:
--------------------------------------------------------------------------------
1 | # Bevigil
2 | SUBSCAN_BEVIGIL_APIKEY=foo
3 | # BinaryEdge
4 | SUBSCAN_BINARYEDGE_APIKEY=bar
5 | # BufferOver
6 | SUBSCAN_BUFFEROVER_APIKEY=baz
7 | # BuiltWith
8 | SUBSCAN_BUILTWITH_APIKEY=foo
9 | # Censys
10 | SUBSCAN_CENSYS_APIKEY=bar
11 | # CertSpotter
12 | SUBSCAN_CERTSPOTTER_APIKEY=baz
13 | # Chaos
14 | SUBSCAN_CHAOS_APIKEY=foo
15 | # Shodan
16 | SUBSCAN_SHODAN_APIKEY=bar
17 | # VirusTotal
18 | SUBSCAN_VIRUSTOTAL_APIKEY=baz
19 | # WhoisXMLAPI
20 | SUBSCAN_WHOISXMLAPI_APIKEY=foo
21 | # ZoomEye
22 | SUBSCAN_ZOOMEYE_APIKEY=bar
23 | # GitHub Access Token
24 | SUBSCAN_GITHUB_APIKEY=baz
25 | # Netlas
26 | SUBSCAN_NETLAS_APIKEY=foo
27 | # SecurityTrails
28 | SUBSCAN_SECURITYTRAILS_APIKEY=bar
29 | # DNSDumpsterAPI
30 | SUBSCAN_DNSDUMPSTERAPI_APIKEY=baz
31 |
--------------------------------------------------------------------------------
/book/src/user-guide/quickstart/usage/README.md:
--------------------------------------------------------------------------------
1 | # Usage
2 |
3 | This chapter guides you on how to use the `Subscan` to efficiently discover subdomains. Subdomain discovery features are implemented as modular `SubscanModule` components, which are automatically executed when a scan is initiated. For technical insights, check out the [Development](../../../development/index.html) chapter
4 |
5 | ✨ In this section, you'll find detailed instructions for different usage methods
6 |
7 | - How to use the [Subscan CLI](cli.md) for quick and effective subdomain enumeration
8 | - [Run Subscan in a Docker container](docker.md) for a lightweight, portable setup
9 | - Integrating [Subscan as a Crate](crate.md) in your Rust project for seamless integration with your codebase
10 |
--------------------------------------------------------------------------------
/.github/workflows/release-plz.yml:
--------------------------------------------------------------------------------
1 | name: CI-CD
2 |
3 | permissions:
4 | pull-requests: write
5 | contents: write
6 |
7 | on:
8 | push:
9 | branches:
10 | - main
11 |
12 | jobs:
13 | release-plz:
14 | runs-on: ubuntu-latest
15 | steps:
16 | - name: Checkout repository
17 | uses: actions/checkout@v6
18 | with:
19 | token: ${{ secrets.RELEASE_PLZ_TOKEN }}
20 | fetch-depth: 0
21 |
22 | - name: Install Rust toolchain
23 | uses: dtolnay/rust-toolchain@stable
24 |
25 | - name: Run release-plz
26 | uses: release-plz/action@v0.5
27 | env:
28 | GITHUB_TOKEN: ${{ secrets.RELEASE_PLZ_TOKEN }}
29 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
30 |
--------------------------------------------------------------------------------
/src/cli/commands/mod.rs:
--------------------------------------------------------------------------------
1 | /// Brute command to start brute force attack on any domain address
2 | pub mod brute;
3 | /// Module command to manage implemented modules
4 | pub mod module;
5 | /// Scan command to start scan on any domain address
6 | pub mod scan;
7 |
8 | use clap::Subcommand;
9 |
10 | use crate::cli::commands::{
11 | brute::BruteCommandArgs, module::ModuleCommandArgs, scan::ScanCommandArgs,
12 | };
13 |
14 | /// List of CLI commands
15 | #[derive(Clone, Debug, Subcommand)]
16 | pub enum Commands {
17 | /// Start scan on any domain address
18 | Scan(ScanCommandArgs),
19 | /// Start brute force attack with a given wordlist
20 | Brute(BruteCommandArgs),
21 | /// Subcommand to manage implemented modules
22 | Module(ModuleCommandArgs),
23 | }
24 |
--------------------------------------------------------------------------------
/book/src/development/README.md:
--------------------------------------------------------------------------------
1 | # Development
2 |
3 | This chapter provides an in-depth guide for developers on how to contribute to and extend `Subscan`. It covers everything from setting up the development environment to understanding the core architecture and adding new features or modules.
4 |
5 | Here’s a quick overview of the sections included
6 |
7 | - [Setup Development Environment](environment.md)
8 | - [Components](components/index.html)
9 | - [Requesters](components/requesters.md)
10 | - [Extractors](components/extractors.md)
11 | - [Subscan Module](components/module.md)
12 | - [Generic Modules](generics/index.html)
13 | - [Integration](generics/integration.md)
14 | - [Search Engine](generics/engine.md)
15 | - [Integrate Your Module Step by Step](integration.md)
16 |
--------------------------------------------------------------------------------
/src/interfaces/lookup.rs:
--------------------------------------------------------------------------------
1 | use async_trait::async_trait;
2 |
3 | use crate::types::{config::resolver::ResolverConfig, func::AsyncIPResolveFunc};
4 |
5 | /// IP lookup future trait implementation, lookup future is a future object that returns
6 | /// IP address of given domain address so `lookup_host_future` method should return a future
7 | /// object that resolvers IP address
8 | #[async_trait]
9 | pub trait LookUpHostFuture: Send + Sync {
10 | /// Should return `lookup_host` future object that acts according to `config.disabled`
11 | /// value. `lookup_host` future must resolve IP address by given host
12 | async fn lookup_host_future(&self) -> AsyncIPResolveFunc;
13 | /// Should return resolver configurations
14 | async fn config(&self) -> ResolverConfig;
15 | }
16 |
--------------------------------------------------------------------------------
/src/cli/banner.rs:
--------------------------------------------------------------------------------
1 | /// Returns project banner as a text art with a version tag
2 | ///
3 | /// # Banner
4 | ///
5 | /// ```text
6 | /// _
7 | /// | |
8 | /// ___ _ _| |__ ___ ___ __ _ _ __
9 | /// / __| | | | '_ \/ __|/ __/ _` | '_ \
10 | /// \__ \ |_| | |_) \__ \ (_| (_| | | | |
11 | /// |___/\__,_|_.__/|___/\___\__,_|_| |_|
12 | ///
13 | /// ```
14 | pub fn banner() -> String {
15 | format!(
16 | r"
17 | _
18 | | |
19 | ___ _ _| |__ ___ ___ __ _ _ __
20 | / __| | | | '_ \/ __|/ __/ _` | '_ \
21 | \__ \ |_| | |_) \__ \ (_| (_| | | | |
22 | |___/\__,_|_.__/|___/\___\__,_|_| |_|
23 |
24 | v{}
25 | ",
26 | env!("CARGO_PKG_VERSION")
27 | )
28 | }
29 |
--------------------------------------------------------------------------------
/src/utilities/serializers.rs:
--------------------------------------------------------------------------------
1 | use chrono::TimeDelta;
2 | use serde::Serializer;
3 |
4 | /// Serializer method to convert [`TimeDelta`] objects to [`i64`] seconds
5 | ///
6 | /// # Examples
7 | ///
8 | /// ```
9 | /// use subscan::utilities::serializers::td_to_seconds;
10 | /// use chrono::TimeDelta;
11 | /// use serde_json::Serializer;
12 | ///
13 | /// let mut buffer = Vec::new();
14 | /// let mut serializer = Serializer::new(&mut buffer);
15 | ///
16 | /// let serialized = td_to_seconds(&TimeDelta::zero(), &mut serializer);
17 | ///
18 | /// assert_eq!(String::from_utf8(buffer).unwrap(), "0");
19 | /// ```
20 | pub fn td_to_seconds(td: &TimeDelta, serializer: S) -> Result
21 | where
22 | S: Serializer,
23 | {
24 | serializer.serialize_i64(td.num_seconds())
25 | }
26 |
--------------------------------------------------------------------------------
/examples/crate_run_usage.rs:
--------------------------------------------------------------------------------
1 | use std::env;
2 |
3 | use log::LevelFilter::Debug;
4 | use subscan::{types::config::subscan::SubscanConfig, Subscan};
5 |
6 | #[tokio::main]
7 | async fn main() {
8 | let exe = env::current_exe().unwrap();
9 | let exe_name = exe.file_name().unwrap().to_str();
10 |
11 | let args: Vec = env::args().collect();
12 | let (module, target) = (&args[1], &args[2]);
13 |
14 | env_logger::builder().filter_module(exe_name.unwrap(), Debug).init();
15 |
16 | // use default configurations
17 | let config = SubscanConfig::default();
18 |
19 | let subscan = Subscan::from(config);
20 | let result = subscan.run(module, target).await;
21 |
22 | for item in result.items {
23 | log::debug!("{}", item.as_txt())
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/.github/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 |
2 | ## Contributing
3 |
4 | First off, thank you for considering contributing to Subscan! 🎉 Your support helps us build a more powerful and reliable tool
5 |
6 | ## How to Get Started
7 |
8 | To make your contribution process smooth, we've prepared detailed development documentation. You can find everything you need in the [Development](https://www.erdoganyoksul.com/subscan/development/index.html) chapter of project book
9 |
10 | ## Acknowledgements
11 |
12 | We deeply appreciate every contributor's effort and time. Whether it's fixing a bug, suggesting improvements, or adding a new feature, your contributions make Subscan better for everyone
13 |
14 | Thank you for being part of this journey! 🙌
15 |
16 |
--------------------------------------------------------------------------------
/dist-workspace.toml:
--------------------------------------------------------------------------------
1 | [workspace]
2 | members = ["cargo:."]
3 |
4 | # Config for 'dist'
5 | [dist]
6 | # The preferred dist version to use in CI (Cargo.toml SemVer syntax)
7 | cargo-dist-version = "0.29.0"
8 | # CI backends to support
9 | ci = "github"
10 | # The installers to generate for each app
11 | installers = ["shell", "powershell"]
12 | # Target platforms to build apps for (Rust target-triple syntax)
13 | targets = ["aarch64-apple-darwin", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "x86_64-pc-windows-msvc"]
14 | # Path that installers should place binaries in
15 | install-path = "CARGO_HOME"
16 | # Whether to install an updater program
17 | install-updater = false
18 | # Which actions to run on pull requests
19 | pr-run-mode = "upload"
20 | # Ignore out-of-date contents
21 | allow-dirty = ["ci"]
22 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # To get started with Dependabot version updates, you'll need to specify which
2 | # package ecosystems to update and where the package manifests are located.
3 | # Please see the documentation for all configuration options:
4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
5 |
6 | version: 2
7 | updates:
8 | - package-ecosystem: "github-actions"
9 | directory: "/"
10 | schedule:
11 | interval: "weekly"
12 |
13 | - package-ecosystem: "docker"
14 | directory: "/"
15 | schedule:
16 | interval: "weekly"
17 |
18 | - package-ecosystem: "cargo" # See documentation for possible values
19 | directory: "/" # Location of package manifests
20 | schedule:
21 | interval: "weekly"
22 |
--------------------------------------------------------------------------------
/testing/testdata/txt/resolverlist.txt:
--------------------------------------------------------------------------------
1 | INVALID IPV4
2 | ------------
3 | foo
4 | foo:bar
5 | foo:bar:baz
6 | 127.0.0.1:0000
7 | 192.168.44.10:
8 | 192.168.44.10:65537
9 | 256.256.256.256:65535
10 | 256.123.45.81:65535
11 | 123
12 | 123.1231.123.123:65
13 | 123:123:123:123
14 | 4444.44
15 | 127.0.0
16 |
17 | VALID IPV4
18 | ------------
19 | 127.0.0.1:1
20 | 127.9.2.129:25
21 | 176.255.45.12:123
22 | 192.168.1.1:8080
23 | 10.126.125.98:4444
24 | 0.0.0.0:4444
25 |
26 | INVALID IPV6
27 | ------------
28 | foo:123
29 | [foo]:bar
30 | [foo]:123
31 | []:123
32 | [123]:
33 | [foo.bar]:15123
34 | [abcd:1234::123::1]:4213
35 |
36 | VALID IPV6
37 | ------------
38 | [2001:db8::1]:8080
39 | [2001:db8:85a3:8d3:1319:8a2e:370:7348]:443
40 | [abcd:ef::42:1]:8080
41 | [0:0:0:0:0:ffff:1.2.3.4]:4444
42 | [::1]:1234
43 | [::192.168.30.2]:8001
44 |
--------------------------------------------------------------------------------
/tests/stubs/module/integrations/builtwith.json:
--------------------------------------------------------------------------------
1 | {
2 | "request": {
3 | "method": "GET",
4 | "queryParameters": {
5 | "KEY": {
6 | "equalTo": "builtwith-api-key"
7 | }
8 | },
9 | "urlPath": "/builtwith"
10 | },
11 | "response": {
12 | "headers": {
13 | "content-type": "application/json"
14 | },
15 | "jsonBody": {
16 | "Results": [
17 | {
18 | "Result": {
19 | "Paths": [
20 | {
21 | "SubDomain": "bar"
22 | }
23 | ]
24 | }
25 | }
26 | ]
27 | },
28 | "status": 200
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/book/src/development/components/README.md:
--------------------------------------------------------------------------------
1 | # Components
2 |
3 | This chapter provides detailed information about the functionality of the core components that make up `Subscan`. These components are reusable structures designed to simplify repetitive tasks, such as organizing HTTP requests or facilitating subdomain extraction operations. By using these components, you can streamline your workflow and avoid redundant code
4 |
5 | You can also create custom components tailored to your specific needs and integrate them into the subdomain discovery process. These components add modularity to `Subscan`, allowing it to be easily extended and customized
6 |
7 | The core components in `Subscan` are listed below. Follow the links for more details
8 |
9 | - [Requesters](requesters.md)
10 | - [Extractors](extractors.md)
11 | - [Subscan Module](module.md)
12 |
--------------------------------------------------------------------------------
/src/utilities/env.rs:
--------------------------------------------------------------------------------
1 | use crate::constants::SUBSCAN_ENV_NAMESPACE;
2 |
3 | /// Formats given module name and environment variable name with [`SUBSCAN_ENV_NAMESPACE`]
4 | /// prefix, returns fully generated environment variable name
5 | ///
6 | /// # Examples
7 | ///
8 | /// ```
9 | /// use subscan::utilities::env::format_env;
10 | ///
11 | /// #[tokio::main]
12 | /// async fn main() {
13 | /// assert_eq!(format_env("foo", "apikey"), "SUBSCAN_FOO_APIKEY");
14 | /// assert_eq!(format_env("foo", "username"), "SUBSCAN_FOO_USERNAME");
15 | /// assert_eq!(format_env("bar", "password"), "SUBSCAN_BAR_PASSWORD");
16 | /// }
17 | /// ```
18 | pub fn format_env(name: &str, env: &str) -> String {
19 | format!(
20 | "{}_{}_{}",
21 | SUBSCAN_ENV_NAMESPACE,
22 | name.to_uppercase(),
23 | env.to_uppercase(),
24 | )
25 | }
26 |
--------------------------------------------------------------------------------
/src/logger.rs:
--------------------------------------------------------------------------------
1 | use std::io::Write;
2 |
3 | use colog::format::{CologStyle, DefaultCologStyle};
4 | use env_logger::fmt::Formatter;
5 | use log::{LevelFilter, Record};
6 |
7 | use crate::constants::SUBSCAN_BANNER_LOG_TARGET;
8 |
9 | /// Initialize logger
10 | pub async fn init(level: Option) {
11 | let pkg_name = env!("CARGO_PKG_NAME");
12 | let filter = level.unwrap_or(LevelFilter::Debug);
13 |
14 | env_logger::builder().filter_module(pkg_name, filter).format(formatter).init();
15 | }
16 |
17 | // Custom formatter to avoid timestamp and log levels on banner log line
18 | fn formatter(buf: &mut Formatter, record: &Record<'_>) -> Result<(), std::io::Error> {
19 | if record.target() == SUBSCAN_BANNER_LOG_TARGET {
20 | writeln!(buf, "{}", record.args())
21 | } else {
22 | DefaultCologStyle.format(buf, record)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/tests/stubs/module/integrations/whoisxmlapi.json:
--------------------------------------------------------------------------------
1 | {
2 | "request": {
3 | "method": "GET",
4 | "queryParameters": {
5 | "apiKey": {
6 | "equalTo": "whoisxmlapi-api-key"
7 | }
8 | },
9 | "urlPath": "/whoisxmlapi"
10 | },
11 | "response": {
12 | "headers": {
13 | "content-type": "application/json"
14 | },
15 | "jsonBody": {
16 | "result": {
17 | "count": 1,
18 | "records": [
19 | {
20 | "domain": "bar.foo.com",
21 | "firstSeen": 1678185120,
22 | "lastSeen": 1678185120
23 | }
24 | ]
25 | },
26 | "search": "foo.com"
27 | },
28 | "status": 200
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/examples/custom_extractor.rs:
--------------------------------------------------------------------------------
1 | use std::collections::BTreeSet;
2 |
3 | use async_trait::async_trait;
4 | use subscan::{
5 | enums::content::Content,
6 | interfaces::extractor::SubdomainExtractorInterface,
7 | types::core::{Result, Subdomain},
8 | };
9 |
10 | pub struct CustomExtractor {}
11 |
12 | #[async_trait]
13 | impl SubdomainExtractorInterface for CustomExtractor {
14 | async fn extract(&self, content: Content, _domain: &str) -> Result> {
15 | let subdomain = content.as_string().replace("-", "");
16 |
17 | Ok([subdomain].into())
18 | }
19 | }
20 |
21 | #[tokio::main]
22 | async fn main() {
23 | let content = Content::from("--foo.com--");
24 | let extractor = CustomExtractor {};
25 | let result = extractor.extract(content, "foo.com").await.unwrap();
26 |
27 | assert_eq!(result, ["foo.com".into()].into());
28 | }
29 |
--------------------------------------------------------------------------------
/tests/stubs/module/integrations/waybackarchive/waybackarchive.json:
--------------------------------------------------------------------------------
1 | {
2 | "request": {
3 | "method": "GET",
4 | "queryParameters": {
5 | "collapse": {
6 | "equalTo": "urlkey"
7 | },
8 | "fl": {
9 | "equalTo": "original"
10 | },
11 | "output": {
12 | "equalTo": "txt"
13 | },
14 | "url": {
15 | "equalTo": "*.foo.com/*"
16 | }
17 | },
18 | "urlPath": "/waybackarchive"
19 | },
20 | "response": {
21 | "body": "bar.foo.com\nbaz.foo.com",
22 | "headers": {
23 | "accept-ranges": "bytes",
24 | "connection": "keep-alive",
25 | "content-length": 23,
26 | "content-type": "text/html"
27 | },
28 | "status": 200
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/book/src/user-guide/README.md:
--------------------------------------------------------------------------------
1 | # User Guide
2 |
3 | This chapter provides an overview of the basic usage of `Subscan`, designed to help end users get started quickly and effectively
4 |
5 | Here’s a quick overview of the sections included
6 |
7 |
8 |
9 | - [Quickstart](quickstart/index.html)
10 | - [Install](quickstart/install.md)
11 | - [Usage](quickstart/usage/index.html)
12 | - [CLI](quickstart/usage/cli.md)
13 | - [Docker](quickstart/usage/docker.md)
14 | - [Crate](quickstart/usage/crate.md)
15 | - [Commands](commands/index.html)
16 | - [scan](commands/scan.md)
17 | - [brute](commands/brute.md)
18 | - [module](commands/module.md)
19 | - [list](commands/module.md#list)
20 | - [get](commands/module.md#get)
21 | - [run](commands/module.md#run)
22 | - [Environments](environments.md)
23 |
24 |
25 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM rust:1.91-slim-bookworm AS builder
2 |
3 | WORKDIR /builder
4 |
5 | ENV DEBIAN_FRONTEND=noninteractive
6 |
7 | RUN apt-get update -y && \
8 | apt-get install -y --no-install-recommends \
9 | chromium \
10 | libssl-dev \
11 | pkg-config \
12 | tini \
13 | && rm -rf /var/lib/apt/lists/*
14 |
15 | COPY Cargo.toml .
16 | COPY Cargo.lock .
17 | COPY src src
18 |
19 | RUN cargo build --release
20 |
21 | # hadolint ignore=DL3007
22 | FROM gcr.io/distroless/cc-debian12:latest
23 |
24 | ENV SUBSCAN_CHROME_PATH=/usr/lib/chromium/chromium
25 |
26 | # Copy libs from builder
27 | COPY --from=builder /usr/lib /usr/lib
28 | COPY --from=builder /usr/share /usr/share
29 | # Copy required binaries
30 | COPY --from=builder /usr/bin/tini /bin/tini
31 | COPY --from=builder /builder/target/release/subscan /bin/subscan
32 |
33 | WORKDIR /data
34 |
35 | ENTRYPOINT ["tini", "--", "subscan"]
36 |
--------------------------------------------------------------------------------
/tests/stubs/module/integrations/crtsh.json:
--------------------------------------------------------------------------------
1 | {
2 | "request": {
3 | "method": "GET",
4 | "urlPath": "/crtsh"
5 | },
6 | "response": {
7 | "headers": {
8 | "content-type": "application/json"
9 | },
10 | "jsonBody": [
11 | {
12 | "common_name": "bar.foo.com",
13 | "entry_timestamp": "2024-06-14T07:17:12.05",
14 | "id": 13356418102,
15 | "issuer_ca_id": 247125,
16 | "issuer_name": "C=US, O=Foo, CN=Foo RSA 2048 M02",
17 | "name_value": "bar.foo.com",
18 | "not_after": "2025-07-13T23:59:59",
19 | "not_before": "2024-06-14T00:00:00",
20 | "result_count": 1,
21 | "serial_number": "0140a766bf10b20714379fd5ddce96f2"
22 | }
23 | ],
24 | "status": 200
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/cli/mod.rs:
--------------------------------------------------------------------------------
1 | /// CLI banner
2 | pub mod banner;
3 | /// List of CLI commands
4 | pub mod commands;
5 |
6 | use banner::banner;
7 | use clap::Parser;
8 | use clap_verbosity_flag::{DebugLevel, Verbosity};
9 |
10 | use crate::{cli::commands::Commands, constants::SUBSCAN_BANNER_LOG_TARGET, logger};
11 |
12 | /// Data structure for CLI, stores configurations to be used on run-time
13 | #[derive(Clone, Debug, Parser)]
14 | #[command(version, about = banner(), long_about = banner())]
15 | pub struct Cli {
16 | #[command(subcommand)]
17 | pub command: Commands,
18 | #[command(flatten)]
19 | pub verbose: Verbosity,
20 | }
21 |
22 | impl Cli {
23 | pub async fn init(&self) {
24 | logger::init(Some(self.verbose.log_level_filter())).await;
25 | }
26 |
27 | pub async fn banner(&self) {
28 | log::debug!(target: SUBSCAN_BANNER_LOG_TARGET, "{}", banner());
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/utilities/regex.rs:
--------------------------------------------------------------------------------
1 | use core::result::Result;
2 |
3 | use regex::{Error, Regex};
4 |
5 | /// Helper function that generates dynamically regex statement
6 | /// by given domain address to parse subdomains
7 | ///
8 | /// # Examples
9 | ///
10 | /// ```
11 | /// use subscan::utilities::regex::generate_subdomain_regex;
12 | ///
13 | /// let regex_stmt = generate_subdomain_regex("foo.com").unwrap();
14 | ///
15 | /// assert_eq!(regex_stmt.as_str(), "(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.)+(foo\\.com)");
16 | ///
17 | /// assert!(regex_stmt.find("bar.foo.com").is_some());
18 | /// assert!(regex_stmt.find("foo").is_none());
19 | /// ```
20 | pub fn generate_subdomain_regex(domain: &str) -> Result {
21 | let formatted = format!(
22 | r"(?:[a-z0-9](?:[a-z0-9-]{{0,61}}[a-z0-9])?\.)+({domain})",
23 | domain = domain.replace('.', r"\.")
24 | );
25 |
26 | Regex::new(&formatted)
27 | }
28 |
--------------------------------------------------------------------------------
/tests/stubs/module/integrations/waybackarchive/waybackarchive-delayed.json:
--------------------------------------------------------------------------------
1 | {
2 | "request": {
3 | "method": "GET",
4 | "queryParameters": {
5 | "collapse": {
6 | "equalTo": "urlkey"
7 | },
8 | "fl": {
9 | "equalTo": "original"
10 | },
11 | "output": {
12 | "equalTo": "txt"
13 | },
14 | "url": {
15 | "equalTo": "*.foo.com/*"
16 | }
17 | },
18 | "urlPath": "/waybackarchive-delayed"
19 | },
20 | "response": {
21 | "body": "bar.foo.com\nbaz.foo.com",
22 | "fixedDelayMilliseconds": 1000,
23 | "headers": {
24 | "accept-ranges": "bytes",
25 | "connection": "keep-alive",
26 | "content-length": 23,
27 | "content-type": "text/html"
28 | },
29 | "status": 200
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/tests/components/resolver_test.rs:
--------------------------------------------------------------------------------
1 | use subscan::{interfaces::lookup::LookUpHostFuture, types::config::resolver::ResolverConfig};
2 |
3 | use crate::common::{
4 | constants::{LOCAL_HOST, TEST_DOMAIN},
5 | mock::resolver::MockResolver,
6 | };
7 |
8 | #[tokio::test]
9 | async fn lookup_host_future_test_with_returns_none() {
10 | let rconfig = ResolverConfig {
11 | disabled: true,
12 | ..Default::default()
13 | };
14 | let resolver = MockResolver::boxed(rconfig);
15 | let lookup_host = resolver.lookup_host_future().await;
16 |
17 | assert!(lookup_host(TEST_DOMAIN.into()).await.is_none());
18 | }
19 |
20 | #[tokio::test]
21 | async fn lookup_host_future_test_with_returns_ip() {
22 | let resolver = MockResolver::default_boxed();
23 | let lookup_host = resolver.lookup_host_future().await;
24 | let ip = lookup_host(TEST_DOMAIN.into()).await.unwrap();
25 |
26 | assert_eq!(ip.to_string(), LOCAL_HOST);
27 | }
28 |
--------------------------------------------------------------------------------
/.github/workflows/tests.yml:
--------------------------------------------------------------------------------
1 | name: Tests
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | branches:
9 | - main
10 |
11 | env:
12 | RUSTFLAGS: "-Dwarnings"
13 | CARGO_TERM_COLOR: "always"
14 |
15 | jobs:
16 | rust-cargo-test:
17 | strategy:
18 | matrix:
19 | os: [ubuntu-latest, windows-latest, macos-latest]
20 |
21 | runs-on: ${{ matrix.os }}
22 | steps:
23 | - uses: actions/checkout@v6
24 | - uses: dtolnay/rust-toolchain@stable
25 | - uses: Swatinem/rust-cache@v2
26 | - uses: taiki-e/install-action@nextest
27 | - uses: browser-actions/setup-chrome@v2
28 | id: setup-chrome
29 | with:
30 | chrome-version: latest
31 |
32 | - name: Run nextest
33 | run: make nextest
34 | env:
35 | SUBSCAN_CHROME_PATH: ${{ steps.setup-chrome.outputs.chrome-path }}
36 |
37 | - name: Run doc-test
38 | run: make doc-test
39 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | # Rust specific
2 | target/
3 | **/*.rs.bk
4 | *.pdb
5 |
6 | # Cache directories
7 | .cargo-cache/
8 | .cargo/
9 |
10 | # Tests and examples
11 | tests/
12 | examples/
13 | benches/
14 |
15 | # Documentation
16 | docs/
17 | *.md
18 | README*
19 |
20 | # Docker-related files
21 | Dockerfile*
22 | .dockerignore
23 | docker-compose*
24 |
25 | # Git-related files
26 | .git/
27 | .gitignore
28 |
29 | # Logs, databases and temporary files
30 | *.sqlite
31 | *.db
32 | *.log
33 | *.tmp
34 | *.out
35 |
36 | # Temporary folders
37 | tmp/
38 | temp/
39 |
40 | # Project specific backups
41 | backup/
42 | *.bak
43 | *~
44 |
45 | # Envs
46 | .env
47 | .env.*
48 | *.env
49 |
50 | # IDE/Editor-specific files
51 | *.rs.bk
52 | *.swp
53 | *.swo
54 | *.swn
55 | *.bak
56 | *.tmp
57 | *.iml
58 | .project
59 | .classpath
60 | .idea/
61 | .vscode/
62 | .settings/
63 |
64 | # OS-generated files
65 | .DS_Store
66 | Thumbs.db
67 | .Spotlight-V100
68 | ehthumbs.db
69 | .Trashes
70 | ._*
71 |
--------------------------------------------------------------------------------
/src/cli/commands/module/mod.rs:
--------------------------------------------------------------------------------
1 | /// Get command to fetch any module by name
2 | pub mod get;
3 | /// List command to list modules with details
4 | pub mod list;
5 | /// Run command to start any module
6 | pub mod run;
7 |
8 | use clap::{Args, Subcommand};
9 |
10 | use crate::cli::commands::module::{
11 | get::ModuleGetSubCommandArgs, list::ModuleListSubCommandArgs, run::ModuleRunSubCommandArgs,
12 | };
13 |
14 | /// List of subcommands on module command
15 | #[derive(Debug, Clone, Subcommand)]
16 | pub enum ModuleSubCommands {
17 | /// Run a single module by name
18 | Run(ModuleRunSubCommandArgs),
19 | /// List all registered modules with their details
20 | List(ModuleListSubCommandArgs),
21 | /// Get a single module details
22 | Get(ModuleGetSubCommandArgs),
23 | }
24 |
25 | /// Module subcommand container
26 | #[derive(Args, Clone, Debug)]
27 | pub struct ModuleCommandArgs {
28 | #[command(subcommand)]
29 | pub command: ModuleSubCommands,
30 | }
31 |
--------------------------------------------------------------------------------
/tests/stubs/module/integrations/github/github-code-search-no-data.json:
--------------------------------------------------------------------------------
1 | {
2 | "request": {
3 | "headers": {
4 | "Authorization": {
5 | "equalTo": "token github-api-key"
6 | }
7 | },
8 | "method": "GET",
9 | "queryParameters": {
10 | "order": {
11 | "equalTo": "asc"
12 | },
13 | "per_page": {
14 | "equalTo": "100"
15 | },
16 | "q": {
17 | "equalTo": "foo.com"
18 | },
19 | "sort": {
20 | "equalTo": "created"
21 | }
22 | },
23 | "urlPath": "/github-code-search-no-data"
24 | },
25 | "response": {
26 | "headers": {
27 | "content-type": "text/html"
28 | },
29 | "jsonBody": {},
30 | "status": 200,
31 | "transformers": [
32 | "response-template"
33 | ]
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/interfaces/extractor.rs:
--------------------------------------------------------------------------------
1 | use std::collections::BTreeSet;
2 |
3 | use async_trait::async_trait;
4 | use enum_dispatch::enum_dispatch;
5 |
6 | use crate::{
7 | enums::{content::Content, dispatchers::SubdomainExtractorDispatcher},
8 | extractors::{html::HTMLExtractor, json::JSONExtractor, regex::RegexExtractor},
9 | types::core::{Result, Subdomain},
10 | };
11 |
12 | /// Extractor trait definition to implement subdomain extractors
13 | ///
14 | /// All subdomain extractors that implemented in the future must be compatible with this
15 | /// trait. Basically it has single `extract` method like a `main` method. It should
16 | /// extract subdomain addresses and return them from given [`String`] content
17 | #[async_trait]
18 | #[enum_dispatch]
19 | pub trait SubdomainExtractorInterface: Send + Sync {
20 | /// Generic extract method, it should extract subdomain addresses
21 | /// from given [`Content`]
22 | async fn extract(&self, content: Content, domain: &str) -> Result>;
23 | }
24 |
--------------------------------------------------------------------------------
/tests/stubs/module/integrations/github/github-code-search-delayed.json:
--------------------------------------------------------------------------------
1 | {
2 | "request": {
3 | "headers": {
4 | "Authorization": {
5 | "equalTo": "token github-api-key"
6 | }
7 | },
8 | "method": "GET",
9 | "queryParameters": {
10 | "order": {
11 | "equalTo": "asc"
12 | },
13 | "per_page": {
14 | "equalTo": "100"
15 | },
16 | "q": {
17 | "equalTo": "foo.com"
18 | },
19 | "sort": {
20 | "equalTo": "created"
21 | }
22 | },
23 | "urlPath": "/github-code-search-delayed"
24 | },
25 | "response": {
26 | "fixedDelayMilliseconds": 1000,
27 | "headers": {
28 | "content-type": "text/html"
29 | },
30 | "jsonBody": {},
31 | "status": 200,
32 | "transformers": [
33 | "response-template"
34 | ]
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/examples/crate_brute_usage.rs:
--------------------------------------------------------------------------------
1 | use std::{env, io::Write};
2 |
3 | use log::LevelFilter::Debug;
4 | use subscan::{types::config::subscan::SubscanConfig, Subscan};
5 | use tempfile::NamedTempFile;
6 |
7 | #[tokio::main]
8 | async fn main() {
9 | let exe = env::current_exe().unwrap();
10 | let exe_name = exe.file_name().unwrap().to_str();
11 | let args: Vec = env::args().collect();
12 |
13 | env_logger::builder().filter_module(exe_name.unwrap(), Debug).init();
14 |
15 | let mut wordlist = NamedTempFile::new().unwrap();
16 |
17 | writeln!(wordlist, "api").unwrap();
18 | writeln!(wordlist, "app").unwrap();
19 | writeln!(wordlist, "test").unwrap();
20 |
21 | let config = SubscanConfig {
22 | wordlist: Some(wordlist.path().to_path_buf()),
23 | ..Default::default()
24 | };
25 |
26 | let subscan = Subscan::from(config);
27 | let result = subscan.brute(&args[1]).await;
28 |
29 | for item in result.items {
30 | log::debug!("{}", item.as_txt())
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/types/result/pool.rs:
--------------------------------------------------------------------------------
1 | use super::{item::SubscanResultItem, statistics::SubscanModuleStatistic};
2 | use crate::types::result::{item::SubscanResultItems, statistics::SubscanResultStatistics};
3 |
4 | /// Stores [`SubscanModulePool`](crate::pools::module::SubscanModulePool) results
5 | #[derive(Clone, Debug, Default)]
6 | pub struct PoolResult {
7 | /// Pool statistics, includes each module statistics
8 | /// and IP resolver statistics
9 | pub statistics: SubscanResultStatistics,
10 | /// Subdomains that have been discovered
11 | pub items: SubscanResultItems,
12 | }
13 |
14 | impl PoolResult {
15 | pub async fn insert(&mut self, module: &str, subdomain: SubscanResultItem) -> bool {
16 | let defaults = SubscanModuleStatistic::default();
17 | let inserted = self.items.insert(subdomain);
18 |
19 | let stats = self.statistics.entry(module.to_owned()).or_insert(defaults);
20 |
21 | if inserted {
22 | stats.count += 1;
23 | }
24 |
25 | inserted
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/tests/stubs/module/integrations/netlas/with-count/netlas-domains-download.json:
--------------------------------------------------------------------------------
1 | {
2 | "request": {
3 | "bodyPatterns": [
4 | {
5 | "equalToJson": {
6 | "fields": [
7 | "*"
8 | ],
9 | "q": "domain:(domain:*.foo.com AND NOT domain:foo.com)",
10 | "size": 1,
11 | "source_type": "include"
12 | }
13 | }
14 | ],
15 | "headers": {
16 | "X-API-Key": {
17 | "equalTo": "netlas-api-key"
18 | }
19 | },
20 | "method": "POST",
21 | "urlPath": "/api/domains/download/"
22 | },
23 | "response": {
24 | "headers": {
25 | "content-type": "application/json"
26 | },
27 | "jsonBody": [
28 | {
29 | "data": {
30 | "domain": "bar.foo.com"
31 | }
32 | }
33 | ],
34 | "status": 200
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/examples/custom_requester.rs:
--------------------------------------------------------------------------------
1 | use async_trait::async_trait;
2 | use reqwest::Url;
3 | use subscan::{
4 | enums::content::Content,
5 | interfaces::requester::RequesterInterface,
6 | types::{config::requester::RequesterConfig, core::Result},
7 | };
8 |
9 | pub struct CustomRequester {
10 | config: RequesterConfig,
11 | }
12 |
13 | #[async_trait]
14 | impl RequesterInterface for CustomRequester {
15 | async fn config(&mut self) -> &mut RequesterConfig {
16 | &mut self.config
17 | }
18 |
19 | async fn configure(&mut self, config: RequesterConfig) {
20 | self.config = config;
21 | }
22 |
23 | async fn get_content(&self, _url: Url) -> Result {
24 | Ok(Content::Empty)
25 | }
26 | }
27 |
28 | #[tokio::main]
29 | async fn main() {
30 | let url = Url::parse("https://example.com").unwrap();
31 | let requester = CustomRequester {
32 | config: RequesterConfig::default(),
33 | };
34 |
35 | let content = requester.get_content(url).await.unwrap();
36 |
37 | assert_eq!(content.as_string(), "");
38 | }
39 |
--------------------------------------------------------------------------------
/book/src/user-guide/quickstart/install.md:
--------------------------------------------------------------------------------
1 | # Install
2 |
3 | There are several ways to install `Subscan`, depending on your preferences. You can install it via [Cargo](#install-with-cargo) (Rust's package manager), use [Docker](#pull-docker-image) for containerized environments, or download prebuilt [cross-platform binaries](#download-prebuilt-binaries). Choose the method that works best for your setup
4 |
5 | ## Install With Cargo
6 |
7 | 🦀 Install the subscan tool using Cargo, Rust's package manager. Make sure you have [Rust](https://www.rust-lang.org/) installed on your system. Then, run:
8 |
9 | ```bash
10 | ~$ cargo install subscan
11 | ```
12 |
13 | ## Pull Docker Image
14 |
15 | 🐳 For containerized usage, you can pull the subscan Docker image directly from [Docker Hub](https://hub.docker.com/)
16 |
17 | ```bash
18 | ~$ docker pull eredotpkfr/subscan:latest
19 | ```
20 |
21 | ## Download Prebuilt Binaries
22 |
23 | 📦 Prebuilt cross-platform binaries are available on the [releases page](https://github.com/eredotpkfr/subscan/releases). Download the one compatible with your operating system
24 |
--------------------------------------------------------------------------------
/tests/components/modules/engines/duckduckgo_test.rs:
--------------------------------------------------------------------------------
1 | use subscan::{
2 | enums::dispatchers::SubscanModuleDispatcher, modules::engines::duckduckgo::DuckDuckGo,
3 | requesters::client::HTTPClient, types::result::status::SubscanModuleStatus,
4 | };
5 | use tokio::sync::Mutex;
6 |
7 | use crate::common::{
8 | constants::{TEST_BAR_SUBDOMAIN, TEST_DOMAIN},
9 | mock::funcs,
10 | utils,
11 | };
12 |
13 | #[tokio::test]
14 | #[stubr::mock("module/engines/duckduckgo.json")]
15 | async fn run_test() {
16 | let mut duckduckgo = DuckDuckGo::dispatcher();
17 | let new_requester = HTTPClient::default();
18 |
19 | funcs::wrap_module_url(&mut duckduckgo, &stubr.uri());
20 |
21 | if let SubscanModuleDispatcher::GenericSearchEngineModule(ref mut duckduckgo) = duckduckgo {
22 | duckduckgo.components.requester = Mutex::new(new_requester.into());
23 | }
24 |
25 | let (results, status) = utils::run_module(duckduckgo, TEST_DOMAIN).await;
26 |
27 | assert_eq!(results, [TEST_BAR_SUBDOMAIN.into()].into());
28 | assert_eq!(status, SubscanModuleStatus::Finished);
29 | }
30 |
--------------------------------------------------------------------------------
/tests/stubs/module/integrations/certspotter.json:
--------------------------------------------------------------------------------
1 | {
2 | "request": {
3 | "headers": {
4 | "Authorization": {
5 | "equalTo": "certspotter-api-key"
6 | }
7 | },
8 | "method": "GET",
9 | "urlPath": "/certspotter"
10 | },
11 | "response": {
12 | "headers": {
13 | "content-type": "application/json"
14 | },
15 | "jsonBody": [
16 | {
17 | "cert_sha256": "360c41cbc12e1006f817f61819be80a892af7acf4cff89b43e90d0892d1ff221",
18 | "dns_names": [
19 | "bar.foo.com"
20 | ],
21 | "id": "8304288493",
22 | "not_after": "2024-12-30T12:46:39Z",
23 | "not_before": "2024-10-01T12:46:40Z",
24 | "pubkey_sha256": "abaddb27dfb94543a30d58656e2b0187889c26e5fd47c54cac1f568ecd99b3da",
25 | "revoked": false,
26 | "tbs_sha256": "79e9457e879c77d5a2306f0378456c0ef84125fbb593755886215fcb5b1f8c36c"
27 | }
28 | ],
29 | "status": 200
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Erdoğan YOKSUL
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/examples/crate_scan_usage.rs:
--------------------------------------------------------------------------------
1 | use std::env;
2 |
3 | use log::LevelFilter::Debug;
4 | use subscan::{
5 | enums::cache::CacheFilter::FilterByName,
6 | types::{config::subscan::SubscanConfig, filters::ModuleNameFilter},
7 | Subscan,
8 | };
9 |
10 | #[tokio::main]
11 | async fn main() {
12 | let exe = env::current_exe().unwrap();
13 | let exe_name = exe.file_name().unwrap().to_str();
14 | let args: Vec = env::args().collect();
15 |
16 | env_logger::builder().filter_module(exe_name.unwrap(), Debug).init();
17 |
18 | // filter modules by name, only runs google and alienvault modules
19 | let filter = ModuleNameFilter {
20 | modules: vec!["google".into(), "alienvault".into()],
21 | skips: vec![],
22 | };
23 |
24 | // set module conccurrency to 1
25 | let config = SubscanConfig {
26 | concurrency: 1,
27 | filter: FilterByName(filter),
28 | ..Default::default()
29 | };
30 |
31 | let subscan = Subscan::from(config);
32 | let result = subscan.scan(&args[1]).await;
33 |
34 | for item in result.items {
35 | log::debug!("{}", item.as_txt())
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/.github/workflows/mdbook.yml:
--------------------------------------------------------------------------------
1 | name: Github Pages
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | branches:
9 | - main
10 |
11 | jobs:
12 | mdbook-tests:
13 | runs-on: ubuntu-latest
14 | steps:
15 | - uses: actions/checkout@v6
16 |
17 | - name: Setup mdBook
18 | uses: peaceiris/actions-mdbook@v2
19 | with:
20 | mdbook-version: 'latest'
21 |
22 | - name: Run mdBook tests
23 | run: make book-test
24 |
25 | mdbook-deploy:
26 | runs-on: ubuntu-latest
27 | permissions:
28 | contents: write
29 | needs: mdbook-tests
30 | steps:
31 | - uses: actions/checkout@v6
32 |
33 | - name: Setup mdBook
34 | uses: peaceiris/actions-mdbook@v2
35 | with:
36 | mdbook-version: 'latest'
37 |
38 | - name: Build mdBook
39 | run: make book-build
40 |
41 | - name: Deploy to GitHub Pages
42 | uses: peaceiris/actions-gh-pages@v4
43 | if: ${{ github.event_name == 'push' && github.ref_name == 'main' }}
44 | with:
45 | github_token: ${{ secrets.GITHUB_TOKEN }}
46 | publish_dir: book/book
47 |
--------------------------------------------------------------------------------
/tests/components/extractors/regex_test.rs:
--------------------------------------------------------------------------------
1 | use std::collections::BTreeSet;
2 |
3 | use subscan::{
4 | enums::content::Content, extractors::regex::RegexExtractor,
5 | interfaces::extractor::SubdomainExtractorInterface,
6 | };
7 |
8 | use crate::common::constants::{TEST_BAR_SUBDOMAIN, TEST_BAZ_SUBDOMAIN, TEST_DOMAIN};
9 |
10 | #[tokio::test]
11 | async fn extract_one_test() {
12 | let extractor = RegexExtractor::default();
13 |
14 | let matches = String::from(TEST_BAR_SUBDOMAIN);
15 | let no_matches = String::from("foobarbaz");
16 |
17 | assert!(extractor.extract_one(matches, TEST_DOMAIN).is_some());
18 | assert!(extractor.extract_one(no_matches, TEST_DOMAIN).is_none());
19 | }
20 |
21 | #[tokio::test]
22 | async fn extract_test() {
23 | let content = Content::from("bar.foo.com\nbaz.foo.com");
24 |
25 | let extractor = RegexExtractor::default();
26 | let result = extractor.extract(content, TEST_DOMAIN).await;
27 |
28 | let expected = BTreeSet::from([
29 | TEST_BAR_SUBDOMAIN.to_string(),
30 | TEST_BAZ_SUBDOMAIN.to_string(),
31 | ]);
32 |
33 | assert!(result.is_ok());
34 | assert_eq!(result.unwrap(), expected);
35 | }
36 |
--------------------------------------------------------------------------------
/tests/stubs/module/integrations/censys.json:
--------------------------------------------------------------------------------
1 | {
2 | "request": {
3 | "headers": {
4 | "Authorization": {
5 | "equalTo": "censys-api-key"
6 | }
7 | },
8 | "method": "GET",
9 | "queryParameters": {
10 | "cursor": {
11 | "absent": true
12 | }
13 | },
14 | "urlPath": "/censys"
15 | },
16 | "response": {
17 | "headers": {
18 | "content-type": "application/json"
19 | },
20 | "jsonBody": {
21 | "result": {
22 | "hits": [
23 | {
24 | "names": [
25 | "bar.foo.com",
26 | "*.bar.foo.com"
27 | ]
28 | },
29 | {
30 | "names": {
31 | "foo": "bar"
32 | }
33 | }
34 | ],
35 | "links": {
36 | "next": "cursor"
37 | }
38 | }
39 | },
40 | "status": 200
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/interfaces/requester.rs:
--------------------------------------------------------------------------------
1 | use async_trait::async_trait;
2 | use enum_dispatch::enum_dispatch;
3 | use reqwest::Url;
4 |
5 | use crate::{
6 | enums::{content::Content, dispatchers::RequesterDispatcher},
7 | requesters::{chrome::ChromeBrowser, client::HTTPClient},
8 | types::{config::requester::RequesterConfig, core::Result},
9 | };
10 |
11 | /// Generic HTTP client trait definition to implement different HTTP requester objects
12 | /// with a single interface compatible
13 | ///
14 | /// Other requesters that will be implemented in the future must conform to this interface.
15 | /// Mostly uses to get string content from any URL with a single stupid `get_content` method
16 | #[async_trait]
17 | #[enum_dispatch]
18 | pub trait RequesterInterface: Sync + Send {
19 | /// Returns requester configurations as a [`RequesterConfig`] object
20 | async fn config(&mut self) -> &mut RequesterConfig;
21 | /// Configure current requester object by using new [`RequesterConfig`] object
22 | async fn configure(&mut self, config: RequesterConfig);
23 | /// HTTP GET method implementation to fetch HTML content from given source [`Url`]
24 | async fn get_content(&self, url: Url) -> Result;
25 | }
26 |
--------------------------------------------------------------------------------
/src/types/result/metadata.rs:
--------------------------------------------------------------------------------
1 | use chrono::{serde::ts_seconds, DateTime, TimeDelta, Utc};
2 | use serde::Serialize;
3 |
4 | use crate::utilities::serializers::td_to_seconds;
5 |
6 | /// [`SubscanResult`](crate::types::result::subscan::SubscanResult) metadata struct definition
7 | #[derive(Clone, Debug, Serialize)]
8 | pub struct SubscanResultMetadata {
9 | pub target: String,
10 | #[serde(with = "ts_seconds")]
11 | pub started_at: DateTime,
12 | #[serde(with = "ts_seconds")]
13 | pub finished_at: DateTime,
14 | #[serde(serialize_with = "td_to_seconds")]
15 | pub elapsed: TimeDelta,
16 | }
17 |
18 | impl Default for SubscanResultMetadata {
19 | fn default() -> Self {
20 | Self {
21 | target: String::new(),
22 | started_at: Utc::now(),
23 | finished_at: Utc::now(),
24 | elapsed: TimeDelta::zero(),
25 | }
26 | }
27 | }
28 |
29 | impl From<&str> for SubscanResultMetadata {
30 | fn from(target: &str) -> Self {
31 | Self {
32 | target: target.to_string(),
33 | started_at: Utc::now(),
34 | finished_at: Utc::now(),
35 | elapsed: TimeDelta::zero(),
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/tests/components/modules/integrations/dnsrepo_test.rs:
--------------------------------------------------------------------------------
1 | use subscan::{
2 | enums::content::Content,
3 | modules::integrations::dnsrepo::{DnsRepo, DNSREPO_URL},
4 | types::result::status::SubscanModuleStatus,
5 | };
6 |
7 | use crate::common::{
8 | constants::{TEST_BAR_SUBDOMAIN, TEST_DOMAIN, TEST_URL},
9 | mock::funcs,
10 | utils,
11 | };
12 |
13 | #[tokio::test]
14 | #[stubr::mock("module/integrations/dnsrepo.json")]
15 | async fn run_test() {
16 | let mut dnsrepo = DnsRepo::dispatcher();
17 |
18 | funcs::wrap_module_url(&mut dnsrepo, &stubr.path("/dnsrepo"));
19 |
20 | let (results, status) = utils::run_module(dnsrepo, TEST_DOMAIN).await;
21 |
22 | assert_eq!(results, [TEST_BAR_SUBDOMAIN.into()].into());
23 | assert_eq!(status, SubscanModuleStatus::Finished);
24 | }
25 |
26 | #[tokio::test]
27 | async fn get_query_url_test() {
28 | let url = DnsRepo::get_query_url(TEST_DOMAIN);
29 | let expected = format!("{DNSREPO_URL}/?search={TEST_DOMAIN}");
30 |
31 | assert_eq!(url, expected);
32 | }
33 |
34 | #[tokio::test]
35 | async fn get_next_url_test() {
36 | let url = TEST_URL.parse().unwrap();
37 | let next = DnsRepo::get_next_url(url, Content::Empty);
38 |
39 | assert!(next.is_none());
40 | }
41 |
--------------------------------------------------------------------------------
/src/enums/cache.rs:
--------------------------------------------------------------------------------
1 | use crate::types::filters::ModuleNameFilter;
2 |
3 | /// Cache filter variants, it allows to run filter on module cache
4 | #[derive(Clone, Debug, Default, PartialEq)]
5 | pub enum CacheFilter {
6 | /// Do nothing to eliminate modules from cache
7 | #[default]
8 | NoFilter,
9 | /// Filter modules by their names
10 | FilterByName(ModuleNameFilter),
11 | }
12 |
13 | impl CacheFilter {
14 | /// Check module name is filtered or non-filtered by filter type
15 | ///
16 | /// # Examples
17 | ///
18 | /// ```
19 | /// use subscan::enums::cache::CacheFilter;
20 | /// use subscan::types::filters::ModuleNameFilter;
21 | ///
22 | /// #[tokio::main]
23 | /// async fn main() {
24 | /// let filter: ModuleNameFilter = (vec![], vec![]).into();
25 | ///
26 | /// assert!(!CacheFilter::NoFilter.is_filtered("foo").await);
27 | /// assert!(!CacheFilter::FilterByName(filter).is_filtered("foo").await);
28 | /// }
29 | /// ```
30 | pub async fn is_filtered(&self, name: &str) -> bool {
31 | match self {
32 | CacheFilter::NoFilter => false,
33 | CacheFilter::FilterByName(filter) => filter.is_filtered(name).await,
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/utilities/http.rs:
--------------------------------------------------------------------------------
1 | use reqwest::Url;
2 |
3 | /// Update query params without remove old query params. If the
4 | /// given parameter name non-exists it will append end of the
5 | /// query otherwise it's value will be updated
6 | ///
7 | /// # Examples
8 | ///
9 | /// ```
10 | /// use subscan::utilities::http::update_url_query;
11 | /// use reqwest::Url;
12 | ///
13 | /// let mut url: Url = "https://foo.com".parse().unwrap();
14 | ///
15 | /// update_url_query(&mut url, "a".into(), "b".into());
16 | /// assert_eq!(url.to_string(), "https://foo.com/?a=b");
17 | ///
18 | /// // does not override old `a` parameter
19 | /// update_url_query(&mut url, "x".into(), "y".into());
20 | /// assert_eq!(url.to_string(), "https://foo.com/?a=b&x=y");
21 | ///
22 | /// update_url_query(&mut url, "a".into(), "c".into());
23 | /// assert_eq!(url.to_string(), "https://foo.com/?x=y&a=c");
24 | /// ```
25 | pub fn update_url_query(url: &mut Url, name: &str, value: &str) {
26 | let binding = url.clone();
27 | let pairs = binding.query_pairs();
28 | let old = pairs.filter(|item| item.0.to_lowercase() != name.to_lowercase());
29 |
30 | url.query_pairs_mut()
31 | .clear()
32 | .extend_pairs(old)
33 | .append_pair(name, value)
34 | .finish();
35 | }
36 |
--------------------------------------------------------------------------------
/tests/stubs/module/integrations/github/github-code-search-template.json:
--------------------------------------------------------------------------------
1 | {
2 | "request": {
3 | "headers": {
4 | "Authorization": {
5 | "equalTo": "token github-api-key"
6 | }
7 | },
8 | "method": "GET",
9 | "queryParameters": {
10 | "order": {
11 | "equalTo": "asc"
12 | },
13 | "per_page": {
14 | "equalTo": "100"
15 | },
16 | "q": {
17 | "equalTo": "foo.com"
18 | },
19 | "sort": {
20 | "equalTo": "created"
21 | }
22 | },
23 | "urlPath": "/github-code-search"
24 | },
25 | "response": {
26 | "headers": {
27 | "content-type": "text/html"
28 | },
29 | "jsonBody": {
30 | "items": [
31 | {
32 | "html_url": "http://127.0.0.1:{{port}}/{{request.pathSegments.[0]}}/results"
33 | },
34 | {
35 | "no_html_url": "foo"
36 | }
37 | ]
38 | },
39 | "status": 200,
40 | "transformers": [
41 | "response-template"
42 | ]
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/tests/components/modules/integrations/digitorus_test.rs:
--------------------------------------------------------------------------------
1 | use subscan::{
2 | enums::content::Content,
3 | modules::integrations::digitorus::{Digitorus, DIGITORUS_URL},
4 | types::result::status::SubscanModuleStatus,
5 | };
6 |
7 | use crate::common::{
8 | constants::{TEST_BAR_SUBDOMAIN, TEST_DOMAIN, TEST_URL},
9 | mock::funcs,
10 | utils,
11 | };
12 |
13 | #[tokio::test]
14 | #[stubr::mock("module/integrations/digitorus.json")]
15 | async fn run_test() {
16 | let mut digitorus = Digitorus::dispatcher();
17 |
18 | funcs::wrap_module_url(&mut digitorus, &stubr.path("/digitorus"));
19 |
20 | let (results, status) = utils::run_module(digitorus, TEST_DOMAIN).await;
21 |
22 | assert_eq!(results, [TEST_BAR_SUBDOMAIN.into()].into());
23 | assert_eq!(status, SubscanModuleStatus::Finished);
24 | }
25 |
26 | #[tokio::test]
27 | async fn get_query_url_test() {
28 | let url = Digitorus::get_query_url(TEST_DOMAIN);
29 | let expected = format!("{DIGITORUS_URL}/{TEST_DOMAIN}");
30 |
31 | assert_eq!(url, expected);
32 | }
33 |
34 | #[tokio::test]
35 | async fn get_next_url_test() {
36 | let url = TEST_URL.parse().unwrap();
37 | let next = Digitorus::get_next_url(url, Content::Empty);
38 |
39 | assert!(next.is_none());
40 | }
41 |
--------------------------------------------------------------------------------
/src/enums/output.rs:
--------------------------------------------------------------------------------
1 | use std::fs::File;
2 |
3 | use chrono::Utc;
4 | use clap::ValueEnum;
5 | use derive_more::Display;
6 |
7 | use crate::types::result::output::OutputFile;
8 |
9 | /// Supported output formats for reporting scan results
10 | #[derive(Copy, Clone, Debug, Default, Display, Eq, Ord, PartialEq, PartialOrd, ValueEnum)]
11 | pub enum OutputFormat {
12 | #[display("txt")]
13 | TXT,
14 | #[display("csv")]
15 | CSV,
16 | #[default]
17 | #[display("json")]
18 | JSON,
19 | #[display("html")]
20 | HTML,
21 | }
22 |
23 | impl OutputFormat {
24 | pub async fn get_output_file(&self, domain: &str) -> OutputFile {
25 | let file_name = self.get_output_file_name(domain);
26 | let file = File::create(&file_name).unwrap();
27 |
28 | (file_name, file).into()
29 | }
30 |
31 | fn get_output_file_name(&self, domain: &str) -> String {
32 | let now = Utc::now().timestamp();
33 |
34 | match self {
35 | OutputFormat::TXT => format!("{domain}.{now}.{self}"),
36 | OutputFormat::CSV => format!("{domain}.{now}.{self}"),
37 | OutputFormat::JSON => format!("{domain}.{now}.{self}"),
38 | OutputFormat::HTML => format!("{domain}.{now}.{self}"),
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/tests/components/modules/integrations/sitedossier_test.rs:
--------------------------------------------------------------------------------
1 | use subscan::{
2 | enums::content::Content,
3 | modules::integrations::sitedossier::{Sitedossier, SITEDOSSIER_URL},
4 | types::result::status::SubscanModuleStatus,
5 | };
6 |
7 | use crate::common::{
8 | constants::{TEST_BAR_SUBDOMAIN, TEST_DOMAIN, TEST_URL},
9 | mock::funcs,
10 | utils,
11 | };
12 |
13 | #[tokio::test]
14 | #[stubr::mock("module/integrations/sitedossier.json")]
15 | async fn run_test() {
16 | let mut sitedossier = Sitedossier::dispatcher();
17 |
18 | funcs::wrap_module_url(&mut sitedossier, &stubr.path("/sitedossier"));
19 |
20 | let (results, status) = utils::run_module(sitedossier, TEST_DOMAIN).await;
21 |
22 | assert_eq!(results, [TEST_BAR_SUBDOMAIN.into()].into());
23 | assert_eq!(status, SubscanModuleStatus::Finished);
24 | }
25 |
26 | #[tokio::test]
27 | async fn get_query_url_test() {
28 | let url = Sitedossier::get_query_url(TEST_DOMAIN);
29 | let expected = format!("{SITEDOSSIER_URL}/{TEST_DOMAIN}");
30 |
31 | assert_eq!(url, expected);
32 | }
33 |
34 | #[tokio::test]
35 | async fn get_next_url_test() {
36 | let url = TEST_URL.parse().unwrap();
37 | let next = Sitedossier::get_next_url(url, Content::Empty);
38 |
39 | assert!(next.is_none());
40 | }
41 |
--------------------------------------------------------------------------------
/tests/components/extractors/html_test.rs:
--------------------------------------------------------------------------------
1 | use std::collections::BTreeSet;
2 |
3 | use subscan::{
4 | extractors::html::HTMLExtractor, interfaces::extractor::SubdomainExtractorInterface,
5 | };
6 |
7 | use crate::common::{
8 | constants::{TEST_BAR_SUBDOMAIN, TEST_BAZ_SUBDOMAIN, TEST_DOMAIN},
9 | utils::read_testdata,
10 | };
11 |
12 | #[tokio::test]
13 | async fn extract_without_removes() {
14 | let html = read_testdata("html/subdomains.html");
15 |
16 | let selector = String::from("article > div > a > span:first-child");
17 | let extractor = HTMLExtractor::new(selector, vec![]);
18 | let result = extractor.extract(html, TEST_DOMAIN).await;
19 |
20 | assert_eq!(result.unwrap(), [TEST_BAR_SUBDOMAIN.into()].into());
21 | }
22 |
23 | #[tokio::test]
24 | async fn extract_with_removes() {
25 | let html = read_testdata("html/subdomains-with-removes.html");
26 |
27 | let selector = String::from("article > div > a > span");
28 | let extractor = HTMLExtractor::new(selector, vec!["
".to_string()]);
29 | let result = extractor.extract(html, TEST_DOMAIN).await;
30 |
31 | let expected = BTreeSet::from([
32 | TEST_BAR_SUBDOMAIN.to_string(),
33 | TEST_BAZ_SUBDOMAIN.to_string(),
34 | ]);
35 |
36 | assert_eq!(result.unwrap(), expected);
37 | }
38 |
--------------------------------------------------------------------------------
/.github/workflows/linters.yml:
--------------------------------------------------------------------------------
1 | name: Linters
2 |
3 | on:
4 | pull_request:
5 | branches:
6 | - main
7 |
8 | env:
9 | RUSTDOCFLAGS: "-Dwarnings"
10 | RUSTFLAGS: "-Dwarnings"
11 | CARGO_TERM_COLOR: "always"
12 |
13 | jobs:
14 | rust-lint-checks:
15 | runs-on: ubuntu-latest
16 | steps:
17 | - uses: actions/checkout@v6
18 | - uses: dtolnay/rust-toolchain@nightly
19 | with:
20 | components: rustfmt, clippy
21 | - uses: Swatinem/rust-cache@v2
22 |
23 | - name: Run rustfmt
24 | run: make rustfmt-check
25 |
26 | - name: Run clippy
27 | run: make clippy
28 |
29 | rust-lint-docs:
30 | runs-on: ubuntu-latest
31 | steps:
32 | - uses: actions/checkout@v6
33 | - uses: dtolnay/rust-toolchain@nightly
34 | - uses: Swatinem/rust-cache@v2
35 | - uses: dtolnay/install@cargo-docs-rs
36 |
37 | - name: Run doc-rs
38 | run: make doc-rs
39 |
40 | hadolint:
41 | runs-on: ubuntu-latest
42 | steps:
43 | - uses: actions/checkout@v6
44 | - uses: hadolint/hadolint-action@v3.3.0
45 | with:
46 | dockerfile: Dockerfile
47 |
48 | typos:
49 | runs-on: ubuntu-latest
50 | steps:
51 | - uses: actions/checkout@v6
52 | - uses: crate-ci/typos@master
53 |
--------------------------------------------------------------------------------
/tests/cache_test.rs:
--------------------------------------------------------------------------------
1 | use std::time::Duration;
2 |
3 | use subscan::{
4 | cache::CacheManager,
5 | interfaces::{module::SubscanModuleInterface, requester::RequesterInterface},
6 | types::config::requester::RequesterConfig,
7 | };
8 |
9 | #[tokio::test]
10 | async fn configure_test() {
11 | let manager = CacheManager::default();
12 |
13 | let old_config = RequesterConfig::default();
14 | let new_config = RequesterConfig {
15 | timeout: Duration::from_secs(120),
16 | ..Default::default()
17 | };
18 |
19 | for module in manager.modules().await.iter() {
20 | if let Some(requester) = module.lock().await.requester().await {
21 | assert_eq!(requester.lock().await.config().await, &old_config);
22 | }
23 | }
24 |
25 | manager.configure(new_config.clone()).await;
26 |
27 | for module in manager.modules().await.iter() {
28 | if let Some(requester) = module.lock().await.requester().await {
29 | assert_eq!(requester.lock().await.config().await, &new_config);
30 | }
31 | }
32 | }
33 |
34 | #[tokio::test]
35 | async fn module_test() {
36 | let manager = CacheManager::default();
37 |
38 | assert!(manager.module("foo").await.is_none());
39 | assert!(manager.module("google").await.is_some());
40 | }
41 |
--------------------------------------------------------------------------------
/tests/components/modules/integrations/hackertarget_test.rs:
--------------------------------------------------------------------------------
1 | use subscan::{
2 | enums::content::Content,
3 | modules::integrations::hackertarget::{HackerTarget, HACKERTARGET_URL},
4 | types::result::status::SubscanModuleStatus,
5 | };
6 |
7 | use crate::common::{
8 | constants::{TEST_BAR_SUBDOMAIN, TEST_DOMAIN, TEST_URL},
9 | mock::funcs,
10 | utils,
11 | };
12 |
13 | #[tokio::test]
14 | #[stubr::mock("module/integrations/hackertarget.json")]
15 | async fn run_test() {
16 | let mut hackertarget = HackerTarget::dispatcher();
17 |
18 | funcs::wrap_module_url(&mut hackertarget, &stubr.path("/hackertarget"));
19 |
20 | let (results, status) = utils::run_module(hackertarget, TEST_DOMAIN).await;
21 |
22 | assert_eq!(results, [TEST_BAR_SUBDOMAIN.into()].into());
23 | assert_eq!(status, SubscanModuleStatus::Finished);
24 | }
25 |
26 | #[tokio::test]
27 | async fn get_query_url_test() {
28 | let url = HackerTarget::get_query_url(TEST_DOMAIN);
29 | let expected = format!("{HACKERTARGET_URL}/?q={TEST_DOMAIN}");
30 |
31 | assert_eq!(url, expected);
32 | }
33 |
34 | #[tokio::test]
35 | async fn get_next_url_test() {
36 | let url = TEST_URL.parse().unwrap();
37 | let next = HackerTarget::get_next_url(url, Content::Empty);
38 |
39 | assert!(next.is_none());
40 | }
41 |
--------------------------------------------------------------------------------
/tests/stubs/module/integrations/shodan.json:
--------------------------------------------------------------------------------
1 | {
2 | "request": {
3 | "method": "GET",
4 | "queryParameters": {
5 | "key": {
6 | "equalTo": "shodan-api-key"
7 | },
8 | "page": {
9 | "absent": true
10 | }
11 | },
12 | "urlPath": "/shodan"
13 | },
14 | "response": {
15 | "headers": {
16 | "content-type": "application/json"
17 | },
18 | "jsonBody": {
19 | "data": [
20 | {
21 | "last_seen": "2024-10-06T03:23:59.910000",
22 | "ports": [
23 | 80,
24 | 443,
25 | 2082
26 | ],
27 | "subdomain": "",
28 | "tags": [
29 | "cdn"
30 | ],
31 | "type": "A",
32 | "value": "104.18.174.21"
33 | }
34 | ],
35 | "domain": "foo.com",
36 | "more": true,
37 | "subdomains": [
38 | "bar"
39 | ],
40 | "tags": [
41 | "dmarc"
42 | ]
43 | },
44 | "status": 200
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/types/core.rs:
--------------------------------------------------------------------------------
1 | use std::{result, sync::Arc};
2 |
3 | use derive_more::From;
4 | use flume::{Receiver, Sender};
5 | use tokio::sync::Mutex;
6 |
7 | use crate::{
8 | enums::dispatchers::{
9 | RequesterDispatcher, SubdomainExtractorDispatcher, SubscanModuleDispatcher,
10 | },
11 | error::SubscanError,
12 | };
13 |
14 | /// Result type
15 | pub type Result = result::Result;
16 | /// Core subdomain data type
17 | pub type Subdomain = String;
18 | /// `SubscanModule` type wrapper
19 | pub type SubscanModule = Arc>;
20 |
21 | impl From for SubscanModule {
22 | fn from(module: SubscanModuleDispatcher) -> Self {
23 | Self::new(Mutex::new(module))
24 | }
25 | }
26 |
27 | /// Flume unbounded channel with generic typed
28 | #[derive(From)]
29 | #[from((Sender, Receiver))]
30 | pub struct UnboundedFlumeChannel {
31 | pub tx: Sender,
32 | pub rx: Receiver,
33 | }
34 |
35 | /// Container for core components of `Subscan` modules
36 | pub struct SubscanModuleCoreComponents {
37 | /// Requester object instance for HTTP requests
38 | pub requester: Mutex,
39 | /// Any extractor object to extract subdomain from content
40 | pub extractor: SubdomainExtractorDispatcher,
41 | }
42 |
--------------------------------------------------------------------------------
/book/src/SUMMARY.md:
--------------------------------------------------------------------------------
1 | # Summary
2 |
3 |
4 |
5 | - [Introduction](README.md)
6 | - [User Guide](user-guide/README.md)
7 | - [Quickstart](user-guide/quickstart/README.md)
8 | - [Install](user-guide/quickstart/install.md)
9 | - [Usage](user-guide/quickstart/usage/README.md)
10 | - [CLI](user-guide/quickstart/usage/cli.md)
11 | - [Docker](user-guide/quickstart/usage/docker.md)
12 | - [Crate](user-guide/quickstart/usage/crate.md)
13 | - [Commands](user-guide/commands/README.md)
14 | - [scan](user-guide/commands/scan.md)
15 | - [brute](user-guide/commands/brute.md)
16 | - [module](user-guide/commands/module.md)
17 | - [Environments](user-guide/environments.md)
18 | - [Development](development/README.md)
19 | - [Setup Environment](development/environment.md)
20 | - [Components](development/components/README.md)
21 | - [Requesters](development/components/requesters.md)
22 | - [Extractors](development/components/extractors.md)
23 | - [Subscan Module](development/components/module.md)
24 | - [Generic Modules](development/generics/README.md)
25 | - [Integration](development/generics/integration.md)
26 | - [Search Engine](development/generics/engine.md)
27 | - [Integrate Your Module Step by Step](development/integration.md)
28 |
29 |
30 |
--------------------------------------------------------------------------------
/tests/stubs/module/integrations/commoncrawl/commoncrawl-index-template.json:
--------------------------------------------------------------------------------
1 | {
2 | "request": {
3 | "method": "GET",
4 | "urlPath": "/commoncrawl/index"
5 | },
6 | "response": {
7 | "headers": {
8 | "content-type": "application/json"
9 | },
10 | "jsonBody": [
11 | {
12 | "cdx-api": "http://127.0.0.1:{{port}}/{{request.pathSegments.[0]}}/cdx-1",
13 | "id": "{{now format='yyyy/MM/dd'}}"
14 | },
15 | {
16 | "cdx-api": "http://127.0.0.1:{{port}}/{{request.pathSegments.[0]}}/cdx-2",
17 | "id": "{{now format='yyyy/MM/dd'}}"
18 | },
19 | {
20 | "cdx-api": "http://127.0.0.1:{{port}}/{{request.pathSegments.[0]}}/cdx-3",
21 | "id": "{{now offset='-3 years'}}"
22 | },
23 | {
24 | "cdx-api": "http://127.0.0.1:{{port}}/{{request.pathSegments.[0]}}/cdx-4",
25 | "id": "{{now format='yyyy/MM/dd'}}"
26 | },
27 | {
28 | "no-id-field": "foo"
29 | },
30 | {
31 | "no-cdx-api-field": "foo"
32 | }
33 | ],
34 | "status": 200,
35 | "transformers": [
36 | "response-template"
37 | ]
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/utilities/cli.rs:
--------------------------------------------------------------------------------
1 | use prettytable::{format::consts::FORMAT_NO_LINESEP_WITH_TITLE, row, table, Table};
2 |
3 | /// Creates table for [`SubscanModule`](crate::types::core::SubscanModule)
4 | ///
5 | /// # Examples
6 | ///
7 | /// ```
8 | /// use subscan::utilities::cli;
9 | ///
10 | /// #[tokio::main]
11 | /// async fn main() {
12 | /// let table = cli::create_module_table().await;
13 | ///
14 | /// assert!(table.is_empty());
15 | /// }
16 | /// ```
17 | pub async fn create_module_table() -> Table {
18 | let mut table = table!();
19 |
20 | let titles = row!["Name", "Requester", "Extractor", "Is Generic?"];
21 |
22 | table.set_format(*FORMAT_NO_LINESEP_WITH_TITLE);
23 | table.set_titles(titles);
24 |
25 | table
26 | }
27 |
28 | /// Creates table for [`SubscanResultItem`](crate::types::result::item::SubscanResultItem)
29 | ///
30 | /// # Examples
31 | ///
32 | /// ```
33 | /// use subscan::utilities::cli;
34 | ///
35 | /// #[tokio::main]
36 | /// async fn main() {
37 | /// let table = cli::create_scan_result_item_table().await;
38 | ///
39 | /// assert!(table.is_empty());
40 | /// }
41 | /// ```
42 | pub async fn create_scan_result_item_table() -> Table {
43 | let mut table = table!();
44 |
45 | let titles = row!["Subdomain", "IP"];
46 |
47 | table.set_format(*FORMAT_NO_LINESEP_WITH_TITLE);
48 | table.set_titles(titles);
49 |
50 | table
51 | }
52 |
--------------------------------------------------------------------------------
/src/types/func.rs:
--------------------------------------------------------------------------------
1 | use std::{collections::BTreeSet, net::IpAddr, pin::Pin};
2 |
3 | use futures::Future;
4 | use reqwest::Url;
5 | use serde_json::Value;
6 |
7 | use super::core::{Result, Subdomain};
8 | use crate::enums::content::Content;
9 |
10 | /// Inner extract method type definition for [`JSONExtractor`](crate::extractors::json::JSONExtractor)
11 | /// In summary it takes a [`Value`] as a parameter and parse subdomains
12 | pub type InnerExtractFunc = Box Result> + Sync + Send>;
13 | /// Get query url function, [`GenericIntegrationModule`](crate::modules::generics::integration::GenericIntegrationModule)
14 | /// uses this type to get start query URL
15 | pub type GetQueryUrlFunc = Box String + Sync + Send>;
16 | /// Get next url function, [`GenericIntegrationModule`](crate::modules::generics::integration::GenericIntegrationModule)
17 | /// uses this function to get next query URL for fetch API fully
18 | pub type GetNextUrlFunc = Box Option + Sync + Send>;
19 | /// IP address resolver function type
20 | pub type AsyncIPResolveFunc =
21 | Box Pin> + Send>> + Send + Sync>;
22 |
23 | /// Container for generic integration module functions
24 | pub struct GenericIntegrationCoreFuncs {
25 | pub url: GetQueryUrlFunc,
26 | pub next: GetNextUrlFunc,
27 | }
28 |
--------------------------------------------------------------------------------
/src/bin/subscan.rs:
--------------------------------------------------------------------------------
1 | use std::io;
2 |
3 | use clap::Parser;
4 | use subscan::{
5 | cli::{
6 | commands::{module::ModuleSubCommands, Commands},
7 | Cli,
8 | },
9 | Subscan,
10 | };
11 |
12 | #[tokio::main]
13 | async fn main() {
14 | let cli = Cli::parse();
15 | let subscan = Subscan::from(cli.clone());
16 | let out = &mut io::stdout();
17 |
18 | cli.init().await;
19 | cli.banner().await;
20 |
21 | match cli.command {
22 | Commands::Module(module) => match module.command {
23 | ModuleSubCommands::List(list) => {
24 | list.as_table(subscan.modules().await, out).await;
25 | }
26 | ModuleSubCommands::Get(get) => {
27 | get.as_table(subscan.module(&get.name).await, out).await;
28 | }
29 | ModuleSubCommands::Run(args) => {
30 | subscan.run(&args.name, &args.domain).await.save(&args.output).await;
31 | }
32 | },
33 | Commands::Scan(args) => {
34 | subscan.scan(&args.domain).await.save(&args.output).await;
35 | }
36 | Commands::Brute(args) => {
37 | if args.stream_to_txt.is_some() {
38 | subscan.brute(&args.domain).await;
39 | } else {
40 | subscan.brute(&args.domain).await.save(&args.output).await;
41 | }
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/enums/result.rs:
--------------------------------------------------------------------------------
1 | use derive_more::Deref;
2 |
3 | use crate::types::{
4 | core::Subdomain,
5 | result::{
6 | item::{SubscanModuleResultItem, SubscanModuleStatusItem},
7 | status::SubscanModuleStatus,
8 | },
9 | };
10 |
11 | /// Subscan module result variants
12 | #[derive(Clone, Debug, PartialEq)]
13 | pub enum SubscanModuleResult {
14 | SubscanModuleResultItem(SubscanModuleResultItem),
15 | SubscanModuleStatusItem(SubscanModuleStatusItem),
16 | }
17 |
18 | /// Optional subscan module result type
19 | #[derive(Clone, Debug, Deref)]
20 | pub struct OptionalSubscanModuleResult(pub Option);
21 |
22 | impl From<(&str, &Subdomain)> for OptionalSubscanModuleResult {
23 | fn from(values: (&str, &Subdomain)) -> Self {
24 | Self(Some(SubscanModuleResult::SubscanModuleResultItem(
25 | values.into(),
26 | )))
27 | }
28 | }
29 |
30 | impl From<(&str, SubscanModuleStatus)> for OptionalSubscanModuleResult {
31 | fn from(values: (&str, SubscanModuleStatus)) -> Self {
32 | Self(Some(SubscanModuleResult::SubscanModuleStatusItem(
33 | values.into(),
34 | )))
35 | }
36 | }
37 |
38 | impl From<(&str, &str)> for OptionalSubscanModuleResult {
39 | fn from(values: (&str, &str)) -> Self {
40 | Self(Some(SubscanModuleResult::SubscanModuleStatusItem(
41 | values.into(),
42 | )))
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/.github/workflows/security.yml:
--------------------------------------------------------------------------------
1 | name: Security
2 |
3 | on:
4 | pull_request:
5 | branches:
6 | - main
7 |
8 | env:
9 | RUSTFLAGS: "-Dwarnings"
10 | CARGO_TERM_COLOR: "always"
11 |
12 | jobs:
13 | rust-cargo-deny:
14 | runs-on: ubuntu-latest
15 | steps:
16 | - uses: actions/checkout@v6
17 | - uses: EmbarkStudios/cargo-deny-action@v2
18 | with:
19 | command: check
20 | log-level: error
21 | arguments: --all-features
22 |
23 | rust-cargo-udeps:
24 | runs-on: ubuntu-latest
25 | steps:
26 | - uses: actions/checkout@v6
27 | - uses: dtolnay/rust-toolchain@nightly
28 | - uses: Swatinem/rust-cache@v2
29 | - uses: aig787/cargo-udeps-action@v1
30 | with:
31 | version: "latest"
32 | args: "--all-targets"
33 | env:
34 | RUSTFLAGS: ""
35 |
36 | rust-cargo-machete:
37 | runs-on: ubuntu-latest
38 | steps:
39 | - uses: actions/checkout@v6
40 | - uses: Swatinem/rust-cache@v2
41 | - uses: bnjbvr/cargo-machete@v0.9.1
42 |
43 | gitleaks-scan:
44 | permissions:
45 | contents: read
46 | discussions: write
47 | pull-requests: write
48 | runs-on: ubuntu-latest
49 | steps:
50 | - uses: actions/checkout@v6
51 | with:
52 | fetch-depth: 0
53 | - uses: gitleaks/gitleaks-action@v2
54 | env:
55 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
56 |
--------------------------------------------------------------------------------
/tests/components/common/mock/resolver.rs:
--------------------------------------------------------------------------------
1 | use std::{
2 | net::{IpAddr, Ipv4Addr},
3 | str::FromStr,
4 | };
5 |
6 | use async_trait::async_trait;
7 | use subscan::{
8 | interfaces::lookup::LookUpHostFuture,
9 | types::{config::resolver::ResolverConfig, func::AsyncIPResolveFunc},
10 | };
11 |
12 | use crate::common::constants::LOCAL_HOST;
13 |
14 | #[derive(Default)]
15 | pub struct MockResolver {
16 | config: ResolverConfig,
17 | }
18 |
19 | impl MockResolver {
20 | pub fn new(config: ResolverConfig) -> Self {
21 | Self { config }
22 | }
23 |
24 | pub fn boxed(config: ResolverConfig) -> Box {
25 | Box::new(Self::new(config))
26 | }
27 |
28 | pub fn default_boxed() -> Box {
29 | Box::new(Self::default())
30 | }
31 | }
32 |
33 | impl From for MockResolver {
34 | fn from(config: ResolverConfig) -> Self {
35 | Self { config }
36 | }
37 | }
38 |
39 | #[async_trait]
40 | impl LookUpHostFuture for MockResolver {
41 | async fn lookup_host_future(&self) -> AsyncIPResolveFunc {
42 | if self.config.disabled {
43 | Box::new(|_: String| Box::pin(async move { None }))
44 | } else {
45 | Box::new(move |_: String| {
46 | Box::pin(async move { Some(IpAddr::V4(Ipv4Addr::from_str(LOCAL_HOST).unwrap())) })
47 | })
48 | }
49 | }
50 |
51 | async fn config(&self) -> ResolverConfig {
52 | self.config.clone()
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/tests/components/extractors/json_test.rs:
--------------------------------------------------------------------------------
1 | use std::collections::BTreeSet;
2 |
3 | use serde_json::Value;
4 | use subscan::{
5 | enums::content::Content, error::ModuleErrorKind::JSONExtract, extractors::json::JSONExtractor,
6 | interfaces::extractor::SubdomainExtractorInterface,
7 | };
8 |
9 | use crate::common::{
10 | constants::{TEST_BAR_SUBDOMAIN, TEST_BAZ_SUBDOMAIN, TEST_DOMAIN},
11 | utils::read_testdata,
12 | };
13 |
14 | #[tokio::test]
15 | async fn extract_test() {
16 | let json = read_testdata("json/subdomains.json");
17 |
18 | let inner_parser = |json: Value, _domain: &str| {
19 | if let Some(subdomains) = json["data"]["subdomains"].as_array() {
20 | let filter = |json: &Value| Some(json["subdomain"].as_str().unwrap().to_string());
21 |
22 | return Ok(BTreeSet::from_iter(subdomains.iter().filter_map(filter)));
23 | }
24 |
25 | Err(JSONExtract.into())
26 | };
27 |
28 | let extractor = JSONExtractor::new(Box::new(inner_parser));
29 |
30 | let result = extractor.extract(json, TEST_DOMAIN).await;
31 | let no_result = extractor.extract(Content::default(), TEST_DOMAIN).await;
32 |
33 | let expected = BTreeSet::from([
34 | TEST_BAR_SUBDOMAIN.to_string(),
35 | TEST_BAZ_SUBDOMAIN.to_string(),
36 | ]);
37 |
38 | assert!(result.is_ok());
39 | assert!(no_result.is_err());
40 |
41 | assert_eq!(no_result.err().unwrap(), JSONExtract.into());
42 | assert_eq!(result.unwrap(), expected);
43 | }
44 |
--------------------------------------------------------------------------------
/book/src/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | Subscan is a powerful subdomain enumeration tool built with [Rust](https://www.rust-lang.org/), specifically designed for penetration testing purposes. It combines various discovery techniques into a single, lightweight binary, making subdomain hunting easier and faster for security researchers
10 |
11 | ### Features
12 |
13 | - 🕵️ Smart Discovery Tricks
14 | - Use multiple search engines (`Google`, `Yahoo`, `Bing`, `DuckDuckGo`, etc.)
15 | - Integrate with APIs like `Shodan`, `Censys`, `VirusTotal` and more
16 | - Perform zone transfer checks
17 | - Subdomain brute-forcing with optimized wordlists
18 | - 🔍 Resolve IP addresses for all subdomains
19 | - 📎 Export reports in `CSV`, `HTML`, `JSON`, or `TXT` formats
20 | - 🛠️ Configurable
21 | - Customize HTTP requests (user-agent, timeout, etc.)
22 | - Rotate requests via proxies (`--proxy` argument)
23 | - Fine-tune IP resolver with `--resolver` arguments
24 | - Filter and run specific modules with `--skips` and `--modules`
25 | - 🐳 Docker Friendly
26 | - Native support for `amd64` and `arm64` Linux platforms
27 | - A tiny container that won't eat up your storage — under 1GB and ready to roll 🚀
28 | - 💻 Compatible with multiple platforms and easy to install as a single binary
29 |
--------------------------------------------------------------------------------
/src/cli/commands/brute.rs:
--------------------------------------------------------------------------------
1 | use std::path::PathBuf;
2 |
3 | use clap::Args;
4 |
5 | use crate::{
6 | constants::{DEFAULT_RESOLVER_CONCURRENCY, DEFAULT_RESOLVER_TIMEOUT},
7 | enums::output::OutputFormat,
8 | };
9 |
10 | /// Brute force attack command arguments
11 | #[derive(Args, Clone, Debug, Default)]
12 | pub struct BruteCommandArgs {
13 | /// Target domain address for brute force attack
14 | #[arg(short, long)]
15 | pub domain: String,
16 | /// Wordlist file to be used during attack
17 | #[arg(short, long)]
18 | pub wordlist: PathBuf,
19 | /// If sets, output will be logged on stdout
20 | #[arg(long, default_value_t = false)]
21 | pub print: bool,
22 | /// Optional txt file to create file stream for the subdomains that found.
23 | /// If sets the `--output` parameter will be disabled
24 | #[arg(short, long, default_value = None)]
25 | pub stream_to_txt: Option,
26 | /// Set output format
27 | #[arg(value_enum, short, long, default_value_t = OutputFormat::JSON)]
28 | pub output: OutputFormat,
29 | /// IP resolver timeout value as a milliseconds
30 | #[arg(long, default_value_t = DEFAULT_RESOLVER_TIMEOUT.as_millis() as u64)]
31 | pub resolver_timeout: u64,
32 | /// IP resolver concurrency level, thread counts of resolver instances
33 | #[arg(long, default_value_t = DEFAULT_RESOLVER_CONCURRENCY)]
34 | pub resolver_concurrency: u64,
35 | /// A text file containing list of resolvers. See `resolverlist.template`
36 | #[arg(long, default_value = None)]
37 | pub resolver_list: Option,
38 | }
39 |
--------------------------------------------------------------------------------
/.github/workflows/coverage.yml:
--------------------------------------------------------------------------------
1 | name: Coverage
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | branches:
9 | - main
10 |
11 | env:
12 | RUSTFLAGS: "-Dwarnings"
13 | CARGO_TERM_COLOR: "always"
14 |
15 | jobs:
16 | cargo-llvm-cov:
17 | runs-on: ubuntu-latest
18 | steps:
19 | - uses: actions/checkout@v6
20 | - uses: dtolnay/rust-toolchain@nightly
21 | - uses: Swatinem/rust-cache@v2
22 | - uses: taiki-e/install-action@cargo-llvm-cov
23 |
24 | # We are not using nextest in here because of it has some
25 | # shortcomings while working with cargo-llvm-cov and can cause
26 | # false positive results, just use only cargo-llvm-cov
27 | #
28 | # We are using `coverage-ci` profile to reduce binary sizes
29 | # cause of github action runner disk space limitations. This is
30 | # a bit slow but we have to do it because we don't have
31 | # self-hosted runner right now
32 | - name: Generate code coverage
33 | run: |
34 | cargo +nightly llvm-cov \
35 | --profile coverage-ci \
36 | --all-features \
37 | --workspace \
38 | --doctests \
39 | --lcov \
40 | --output-path lcov.info
41 |
42 | - name: Upload coverage report to Codecov
43 | uses: codecov/codecov-action@v5
44 | if: ${{ !(startsWith(github.head_ref, 'release-plz') || startsWith(github.head_ref, 'dependabot')) }}
45 | with:
46 | token: ${{ secrets.CODECOV_TOKEN }}
47 | files: lcov.info
48 | fail_ci_if_error: true
49 |
--------------------------------------------------------------------------------
/tests/components/modules/integrations/waybackarchive_test.rs:
--------------------------------------------------------------------------------
1 | use std::{collections::BTreeSet, time::Duration};
2 |
3 | use subscan::{
4 | error::ModuleErrorKind::GetContent, modules::integrations::waybackarchive::WaybackArchive,
5 | types::result::status::SubscanModuleStatus,
6 | };
7 |
8 | use crate::common::{
9 | constants::{TEST_BAR_SUBDOMAIN, TEST_BAZ_SUBDOMAIN, TEST_DOMAIN},
10 | mock::funcs,
11 | utils,
12 | };
13 |
14 | #[tokio::test]
15 | #[stubr::mock("module/integrations/waybackarchive/waybackarchive.json")]
16 | async fn run_success_test() {
17 | let mut waybackarchive = WaybackArchive::dispatcher();
18 |
19 | funcs::wrap_module_url(&mut waybackarchive, &stubr.path("/waybackarchive"));
20 |
21 | let (results, status) = utils::run_module(waybackarchive, TEST_DOMAIN).await;
22 |
23 | let expected = BTreeSet::from([
24 | TEST_BAR_SUBDOMAIN.to_string(),
25 | TEST_BAZ_SUBDOMAIN.to_string(),
26 | ]);
27 |
28 | assert_eq!(results, expected);
29 | assert_eq!(status, SubscanModuleStatus::Finished);
30 | }
31 |
32 | #[tokio::test]
33 | #[stubr::mock("module/integrations/waybackarchive/waybackarchive-delayed.json")]
34 | async fn run_timeout_test() {
35 | let mut waybackarchive = WaybackArchive::dispatcher();
36 |
37 | funcs::wrap_module_url(&mut waybackarchive, &stubr.path("/waybackarchive-delayed"));
38 | funcs::set_requester_timeout(&mut waybackarchive, Duration::from_millis(500)).await;
39 |
40 | let (results, status) = utils::run_module(waybackarchive, TEST_DOMAIN).await;
41 |
42 | assert_eq!(results, BTreeSet::new());
43 | assert_eq!(status, GetContent.into());
44 | }
45 |
--------------------------------------------------------------------------------
/book/src/user-guide/quickstart/usage/crate.md:
--------------------------------------------------------------------------------
1 | # Crate Usage
2 |
3 | You can easily add `Subscan` to your code and use its results in your application. Since `Subscan` works asynchronously, you need to use it in `async` code blocks. We recommend using [Tokio](https://tokio.rs/) as the async runtime
4 |
5 | This chapter provides step-by-step guidance on how to integrate `Subscan` into your code. For more detailed usage and additional code examples, visit the project's [docs.rs](https://docs.rs/subscan/latest/subscan/) page or check the [examples/](https://github.com/eredotpkfr/subscan/tree/main/examples) folder in the repository
6 |
7 | 1. Add `subscan` crate into your project dependencies
8 |
9 | ```bash
10 | ~$ cargo add subscan
11 | ```
12 |
13 | 2. Create a new instance and start to use it
14 |
15 | ```rust,ignore
16 | #[tokio::main]
17 | async fn main() {
18 | // set module conccurrency to 1
19 | // set HTTP timeout to 120
20 | let config = SubscanConfig {
21 | concurrency: 1,
22 | filter: CacheFilter::FilterByName(ModuleNameFilter {
23 | modules: vec!["alienvault".into()],
24 | skips: vec![],
25 | }),
26 | requester: RequesterConfig {
27 | timeout: Duration::from_secs(120),
28 | ..Default::default()
29 | },
30 | ..Default::default()
31 | };
32 |
33 | let subscan = Subscan::from(config);
34 | let result = subscan.scan("domain.com").await;
35 |
36 | for item in result.items {
37 | // do something with item
38 | }
39 | }
40 | ```
41 |
--------------------------------------------------------------------------------
/book/src/user-guide/quickstart/usage/docker.md:
--------------------------------------------------------------------------------
1 | # Docker Usage
2 |
3 | Once you’ve [pulled](../../quickstart/install.md#pull-docker-image) the pre-built image from [Docker Hub](https://hub.docker.com/), you can easily run the container to perform subdomain enumeration
4 |
5 | ```bash
6 | ~$ docker run -it --rm eredotpkfr/subscan scan -d example.com
7 | ```
8 |
9 | Specify environment variable via docker `--env`
10 |
11 | ```bash
12 | ~$ docker run -it --rm \
13 | --env SUBSCAN_VIRUSTOTAL_APIKEY=foo \
14 | eredotpkfr/subscan scan -d example.com --modules=virustotal
15 | ```
16 |
17 | Specify `.env` file from your host machine, use `/data` folder
18 |
19 | ```bash
20 | ~$ docker run -it --rm \
21 | --volume="$PWD/.env:/data/.env" \
22 | eredotpkfr/subscan scan -d example.com --skips=commoncrawl
23 | ```
24 |
25 | Saving output reports to host machine, use `/data` folder
26 |
27 | ```bash
28 | ~$ docker run -it --rm \
29 | --volume="$PWD/data:/data" \
30 | eredotpkfr/subscan scan -d example.com
31 | ```
32 |
33 | To specify wordlist into docker container, use `/data` folder
34 |
35 | ```bash
36 | ~$ docker run -it --rm \
37 | --volume="$PWD/wordlist.txt:/data/wordlist.txt" \
38 | eredotpkfr/subscan brute -d example.com \
39 | -w wordlist.txt --print
40 | ```
41 |
42 | ## Build a Docker Image
43 |
44 | To build a Docker image locally, run the following command
45 |
46 | ```bash
47 | ~$ docker build -t subscan .
48 | ```
49 |
50 | > If you encounter memory issues while building on an Apple Silicon machine, you can run Colima with the following parameters
51 | >
52 | > ```bash
53 | > ~$ colima start --cpu 11 --memory 16
54 | > ```
55 |
--------------------------------------------------------------------------------
/tests/components/cli/commands/module/list_test.rs:
--------------------------------------------------------------------------------
1 | use std::io::Cursor;
2 |
3 | use clap::Parser;
4 | use subscan::{cli::Cli, modules::engines::google::Google, types::core::SubscanModule};
5 |
6 | use crate::common::utils;
7 |
8 | #[tokio::test]
9 | #[should_panic]
10 | async fn module_list_parse_error_test() {
11 | Cli::try_parse_from(vec!["subscan", "module", "list", "-x"]).unwrap();
12 | }
13 |
14 | #[tokio::test]
15 | async fn module_list_test() {
16 | let args = vec!["subscan", "module", "list"];
17 | let cli = Cli::try_parse_from(args).unwrap();
18 |
19 | let expected = "\
20 | +--------+------------+---------------+-------------+\n\
21 | | Name | Requester | Extractor | Is Generic? |\n\
22 | +--------+------------+---------------+-------------+\n\
23 | | google | HTTPClient | HTMLExtractor | true |\n\
24 | +--------+------------+---------------+-------------+\n\
25 | ";
26 |
27 | match cli.command {
28 | subscan::cli::commands::Commands::Module(sub) => match sub.command {
29 | subscan::cli::commands::module::ModuleSubCommands::List(args) => {
30 | let mut out = Cursor::new(vec![]);
31 | let module = SubscanModule::from(Google::dispatcher());
32 |
33 | args.as_table(&vec![module], &mut out).await;
34 |
35 | let result = String::from_utf8(out.into_inner()).unwrap();
36 |
37 | assert_eq!(utils::fix_new_lines(&result), expected);
38 | }
39 | _ => panic!("Expected ModuleSubCommands::List"),
40 | },
41 | _ => panic!("Expected Commands::Module"),
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/tests/components/cli/commands/module/get_test.rs:
--------------------------------------------------------------------------------
1 | use std::io::Cursor;
2 |
3 | use clap::Parser;
4 | use subscan::{cli::Cli, modules::engines::google::Google};
5 | use tokio::sync::Mutex;
6 |
7 | use crate::common::utils;
8 |
9 | #[tokio::test]
10 | #[should_panic]
11 | async fn module_get_parse_error_test() {
12 | Cli::try_parse_from(vec!["subscan", "module", "get", "-x"]).unwrap();
13 | }
14 |
15 | #[tokio::test]
16 | async fn module_get_test() {
17 | let args = vec!["subscan", "module", "get", "google"];
18 | let cli = Cli::try_parse_from(args).unwrap();
19 |
20 | let expected = "\
21 | +--------+------------+---------------+-------------+\n\
22 | | Name | Requester | Extractor | Is Generic? |\n\
23 | +--------+------------+---------------+-------------+\n\
24 | | google | HTTPClient | HTMLExtractor | true |\n\
25 | +--------+------------+---------------+-------------+\n\
26 | ";
27 |
28 | match cli.command {
29 | subscan::cli::commands::Commands::Module(sub) => match sub.command {
30 | subscan::cli::commands::module::ModuleSubCommands::Get(args) => {
31 | let mut out = Cursor::new(vec![]);
32 | let module = Mutex::new(Google::dispatcher());
33 |
34 | args.as_table(&module, &mut out).await;
35 |
36 | let result = String::from_utf8(out.into_inner()).unwrap();
37 |
38 | assert_eq!(args.name, "google");
39 | assert_eq!(utils::fix_new_lines(&result), expected);
40 | }
41 | _ => panic!("Expected ModuleSubCommands::Get"),
42 | },
43 | _ => panic!("Expected Commands::Module"),
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/deny.toml:
--------------------------------------------------------------------------------
1 | # Configuration for https://github.com/EmbarkStudios/cargo-deny
2 |
3 | [licenses]
4 | confidence-threshold = 0.8
5 | allow = [
6 | "Unicode-3.0",
7 | "MPL-2.0",
8 | "MIT",
9 | "Apache-2.0",
10 | "ISC",
11 | "BSD-3-Clause",
12 | "GPL-3.0-only",
13 | "GPL-3.0"
14 | ]
15 |
16 | # List of license exceptions
17 | exceptions = [
18 | { allow = ["CDLA-Permissive-2.0"], crate = "webpki-roots" },
19 | { allow = ["LGPL-3.0"], crate = "colog" },
20 | { allow = ["Zlib"], crate = "nanorand" },
21 | { allow = ["Zlib"], crate = "libz-rs-sys" },
22 | { allow = ["Zlib"], crate = "zlib-rs" },
23 | { allow = ["GPL-3.0"], crate = "auto_generate_cdp" },
24 | { allow = ["CC0-1.0"], crate = "ppmd-rust" },
25 | { allow = ["bzip2-1.0.6"], crate = "libbz2-rs-sys" }
26 | ]
27 |
28 | [[licenses.clarify]]
29 | crate = "ring"
30 | expression = "MIT AND ISC AND OpenSSL"
31 |
32 | license-files = [
33 | # Each entry is a crate relative path, and the (opaque) hash of its contents
34 | { path = "LICENSE", hash = 0xbd0eed23 }
35 | ]
36 |
37 | [advisories]
38 | # Add the unmaintained crate here
39 | ignore = [
40 | { id = "RUSTSEC-2024-0384", reason = "Not maintained, but no upgrade option" },
41 | { id = "RUSTSEC-2025-0010", reason = "ring 0.16.20 was released over 4 years ago and isn't maintained" },
42 | { id = "RUSTSEC-2025-0009", reason = "ring::aead::quic::HeaderProtectionKey::new_mask() may panic when overflow checking is enabled" },
43 | { id = "RUSTSEC-2025-0014", reason = "Latest humantime crates.io release is four years old and GitHub repository has not seen commits in four years" },
44 | { id = "RUSTSEC-2025-0052", reason = "unmaintained advisory detected" },
45 | { id = "RUSTSEC-2025-0057", reason = "No safe upgrade is available!" }
46 | ]
47 |
--------------------------------------------------------------------------------
/book/src/development/generics/engine.md:
--------------------------------------------------------------------------------
1 | # Generic Search Engine Module
2 |
3 | The [`GenericSearchEngineModule`](https://docs.rs/subscan/latest/subscan/modules/generics/engine/struct.GenericSearchEngineModule.html) is primarily used for search engine integrations. It performs subdomain discovery by conducting dork searches on search engines and provides a generic implementation for search engines that use the same dork structure. To understand how it works, review the source code on the [docs.rs](https://docs.rs/subscan/latest/subscan/modules/generics/engine/struct.GenericSearchEngineModule.html) page. Additionally, the source code of other module implementations that use this implementation can help guide you in its usage
4 |
5 | A search engine module that uses this internally would look like the example below
6 |
7 | ```rust,ignore
8 | pub const EXAMPLE_MODULE_NAME: &str = "example";
9 | pub const EXAMPLE_SEARCH_URL: &str = "https://www.example.com/search";
10 | pub const EXAMPLE_SEARCH_PARAM: &str = "q";
11 | pub const EXAMPLE_CITE_TAG: &str = "cite";
12 |
13 | pub struct ExampleModule {}
14 |
15 | impl ExampleModule {
16 | pub fn dispatcher() -> SubscanModuleDispatcher {
17 | let url = Url::parse(EXAMPLE_SEARCH_URL);
18 |
19 | let extractor: HTMLExtractor = HTMLExtractor::new(EXAMPLE_CITE_TAG.into(), vec![]);
20 | let requester: RequesterDispatcher = HTTPClient::default().into();
21 |
22 | let generic = GenericSearchEngineModule {
23 | name: EXAMPLE_MODULE_NAME.into(),
24 | param: EXAMPLE_SEARCH_PARAM.into(),
25 | url: url.unwrap(),
26 | components: SubscanModuleCoreComponents {
27 | requester: requester.into(),
28 | extractor: extractor.into(),
29 | },
30 | };
31 |
32 | generic.into()
33 | }
34 | }
35 | ```
36 |
--------------------------------------------------------------------------------
/tests/components/modules/integrations/crtsh_test.rs:
--------------------------------------------------------------------------------
1 | use serde_json::Value;
2 | use subscan::{
3 | enums::content::Content,
4 | error::ModuleErrorKind::JSONExtract,
5 | modules::integrations::crtsh::{Crtsh, CRTSH_URL},
6 | types::result::status::SubscanModuleStatus,
7 | };
8 |
9 | use crate::common::{
10 | constants::{TEST_BAR_SUBDOMAIN, TEST_DOMAIN, TEST_URL},
11 | mock::funcs,
12 | utils,
13 | };
14 |
15 | #[tokio::test]
16 | #[stubr::mock("module/integrations/crtsh.json")]
17 | async fn run_test() {
18 | let mut crtsh = Crtsh::dispatcher();
19 |
20 | funcs::wrap_module_url(&mut crtsh, &stubr.path("/crtsh"));
21 |
22 | let (results, status) = utils::run_module(crtsh, TEST_DOMAIN).await;
23 |
24 | assert_eq!(results, [TEST_BAR_SUBDOMAIN.into()].into());
25 | assert_eq!(status, SubscanModuleStatus::Finished);
26 | }
27 |
28 | #[tokio::test]
29 | async fn get_query_url_test() {
30 | let url = Crtsh::get_query_url(TEST_DOMAIN);
31 | let expected = format!("{CRTSH_URL}/?q={TEST_DOMAIN}&output=json");
32 |
33 | assert_eq!(url, expected);
34 | }
35 |
36 | #[tokio::test]
37 | async fn get_next_url_test() {
38 | let url = TEST_URL.parse().unwrap();
39 | let next = Crtsh::get_next_url(url, Content::Empty);
40 |
41 | assert!(next.is_none());
42 | }
43 |
44 | #[tokio::test]
45 | async fn extract_test() {
46 | let stub = "module/integrations/crtsh.json";
47 | let json = utils::read_stub(stub)["response"]["jsonBody"].clone();
48 |
49 | let extracted = Crtsh::extract(json, TEST_DOMAIN);
50 | let not_extracted = Crtsh::extract(Value::Null, TEST_DOMAIN);
51 |
52 | assert!(extracted.is_ok());
53 | assert!(not_extracted.is_err());
54 |
55 | assert_eq!(extracted.unwrap(), [TEST_BAR_SUBDOMAIN.into()].into());
56 | assert_eq!(not_extracted.err().unwrap(), JSONExtract.into());
57 | }
58 |
--------------------------------------------------------------------------------
/tests/components/modules/integrations/anubis_test.rs:
--------------------------------------------------------------------------------
1 | use serde_json::Value;
2 | use subscan::{
3 | enums::content::Content,
4 | error::ModuleErrorKind::JSONExtract,
5 | modules::integrations::anubis::{Anubis, ANUBIS_URL},
6 | types::result::status::SubscanModuleStatus,
7 | };
8 |
9 | use crate::common::{
10 | constants::{TEST_BAR_SUBDOMAIN, TEST_DOMAIN, TEST_URL},
11 | mock::funcs,
12 | utils,
13 | };
14 |
15 | #[tokio::test]
16 | #[stubr::mock("module/integrations/anubis.json")]
17 | async fn run_test() {
18 | let mut anubis = Anubis::dispatcher();
19 |
20 | funcs::wrap_module_url(&mut anubis, &stubr.path("/anubis"));
21 |
22 | let (results, status) = utils::run_module(anubis, TEST_DOMAIN).await;
23 |
24 | assert_eq!(results, [TEST_BAR_SUBDOMAIN.into()].into());
25 | assert_eq!(status, SubscanModuleStatus::Finished);
26 | }
27 |
28 | #[tokio::test]
29 | async fn get_query_url_test() {
30 | let url = Anubis::get_query_url(TEST_DOMAIN);
31 | let expected = format!("{ANUBIS_URL}/{TEST_DOMAIN}");
32 |
33 | assert_eq!(url, expected);
34 | }
35 |
36 | #[tokio::test]
37 | async fn get_next_url_test() {
38 | let url = TEST_URL.parse().unwrap();
39 | let next = Anubis::get_next_url(url, Content::Empty);
40 |
41 | assert!(next.is_none());
42 | }
43 |
44 | #[tokio::test]
45 | async fn extract_test() {
46 | let stub = "module/integrations/anubis.json";
47 | let json = utils::read_stub(stub)["response"]["jsonBody"].clone();
48 |
49 | let extracted = Anubis::extract(json, TEST_DOMAIN);
50 | let not_extracted = Anubis::extract(Value::Null, TEST_DOMAIN);
51 |
52 | assert!(extracted.is_ok());
53 | assert!(not_extracted.is_err());
54 |
55 | assert_eq!(extracted.unwrap(), [TEST_BAR_SUBDOMAIN.into()].into());
56 | assert_eq!(not_extracted.err().unwrap(), JSONExtract.into());
57 | }
58 |
--------------------------------------------------------------------------------
/tests/components/modules/integrations/leakix_test.rs:
--------------------------------------------------------------------------------
1 | use serde_json::Value;
2 | use subscan::{
3 | enums::content::Content,
4 | error::ModuleErrorKind::JSONExtract,
5 | modules::integrations::leakix::{Leakix, LEAKIX_URL},
6 | types::result::status::SubscanModuleStatus,
7 | };
8 |
9 | use crate::common::{
10 | constants::{TEST_BAR_SUBDOMAIN, TEST_DOMAIN, TEST_URL},
11 | mock::funcs,
12 | utils,
13 | };
14 |
15 | #[tokio::test]
16 | #[stubr::mock("module/integrations/leakix.json")]
17 | async fn run_test() {
18 | let mut leakix = Leakix::dispatcher();
19 |
20 | funcs::wrap_module_url(&mut leakix, &stubr.path("/leakix"));
21 |
22 | let (results, status) = utils::run_module(leakix, TEST_DOMAIN).await;
23 |
24 | assert_eq!(results, [TEST_BAR_SUBDOMAIN.into()].into());
25 | assert_eq!(status, SubscanModuleStatus::Finished);
26 | }
27 |
28 | #[tokio::test]
29 | async fn get_query_url_test() {
30 | let url = Leakix::get_query_url(TEST_DOMAIN);
31 | let expected = format!("{LEAKIX_URL}/subdomains/{TEST_DOMAIN}");
32 |
33 | assert_eq!(url, expected);
34 | }
35 |
36 | #[tokio::test]
37 | async fn get_next_url_test() {
38 | let url = TEST_URL.parse().unwrap();
39 | let next = Leakix::get_next_url(url, Content::Empty);
40 |
41 | assert!(next.is_none());
42 | }
43 |
44 | #[tokio::test]
45 | async fn extract_test() {
46 | let stub = "module/integrations/leakix.json";
47 | let json = utils::read_stub(stub)["response"]["jsonBody"].clone();
48 |
49 | let extracted = Leakix::extract(json, TEST_DOMAIN);
50 | let not_extracted = Leakix::extract(Value::Null, TEST_DOMAIN);
51 |
52 | assert!(extracted.is_ok());
53 | assert!(not_extracted.is_err());
54 |
55 | assert_eq!(extracted.unwrap(), [TEST_BAR_SUBDOMAIN.into()].into());
56 | assert_eq!(not_extracted.err().unwrap(), JSONExtract.into());
57 | }
58 |
--------------------------------------------------------------------------------
/src/enums/auth.rs:
--------------------------------------------------------------------------------
1 | use derive_more::From;
2 |
3 | use crate::types::env::Credentials;
4 |
5 | /// Authentication methods for API calls or HTTP requests. [`GenericIntegrationModule`](crate::modules::generics::integration::GenericIntegrationModule)
6 | /// uses them to apply correct auth method. See the method descriptions to learn how it works
7 | #[derive(From, PartialEq)]
8 | pub enum AuthenticationMethod {
9 | /// Some APIs uses request headers to get API key. If this auth type selected API key
10 | /// will add in request headers with a given header key
11 | APIKeyAsHeader(String),
12 | /// This auth type uses when API require API key as a query param. If this method chose
13 | /// API key will be added in URL as a query param with given parameter key
14 | APIKeyAsQueryParam(String),
15 | /// Use basic HTTP authentication method. If the credentials are not provided, module
16 | /// tries to fetch from environment variables using pre-formatted
17 | /// (see [`format_env`](crate::utilities::env::format_env)) variable names. Module specific
18 | /// variable names looks like `SUBSCAN_FOO_USERNAME`, `SUBSCAN_FOO_PASSWORD`
19 | #[from]
20 | BasicHTTPAuthentication(Credentials),
21 | /// This auth type does nothing for auth
22 | NoAuthentication,
23 | }
24 |
25 | impl AuthenticationMethod {
26 | /// Checks the any auth method selector or not
27 | ///
28 | /// # Examples
29 | ///
30 | /// ```
31 | /// use subscan::enums::auth::AuthenticationMethod;
32 | ///
33 | /// let as_header = AuthenticationMethod::APIKeyAsHeader("X-API-Key".to_string());
34 | /// let no_auth = AuthenticationMethod::NoAuthentication;
35 | ///
36 | /// assert!(as_header.is_set());
37 | /// assert!(!no_auth.is_set());
38 | /// ```
39 | pub fn is_set(&self) -> bool {
40 | self != &Self::NoAuthentication
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/tests/components/modules/integrations/alienvault_test.rs:
--------------------------------------------------------------------------------
1 | use serde_json::Value;
2 | use subscan::{
3 | enums::content::Content,
4 | error::ModuleErrorKind::JSONExtract,
5 | modules::integrations::alienvault::{AlienVault, ALIENVAULT_URL},
6 | types::result::status::SubscanModuleStatus,
7 | };
8 |
9 | use crate::common::{
10 | constants::{TEST_BAR_SUBDOMAIN, TEST_DOMAIN, TEST_URL},
11 | mock::funcs,
12 | utils,
13 | };
14 |
15 | #[tokio::test]
16 | #[stubr::mock("module/integrations/alienvault.json")]
17 | async fn run_test() {
18 | let mut alienvault = AlienVault::dispatcher();
19 |
20 | funcs::wrap_module_url(&mut alienvault, &stubr.path("/alienvault"));
21 |
22 | let (results, status) = utils::run_module(alienvault, TEST_DOMAIN).await;
23 |
24 | assert_eq!(results, [TEST_BAR_SUBDOMAIN.into()].into());
25 | assert_eq!(status, SubscanModuleStatus::Finished);
26 | }
27 |
28 | #[tokio::test]
29 | async fn get_query_url_test() {
30 | let url = AlienVault::get_query_url(TEST_DOMAIN);
31 | let expected = format!("{ALIENVAULT_URL}/{TEST_DOMAIN}/passive_dns");
32 |
33 | assert_eq!(url, expected);
34 | }
35 |
36 | #[tokio::test]
37 | async fn get_next_url_test() {
38 | let url = TEST_URL.parse().unwrap();
39 | let next = AlienVault::get_next_url(url, Content::Empty);
40 |
41 | assert!(next.is_none());
42 | }
43 |
44 | #[tokio::test]
45 | async fn extract_test() {
46 | let stub = "module/integrations/alienvault.json";
47 | let json = utils::read_stub(stub)["response"]["jsonBody"].clone();
48 |
49 | let extracted = AlienVault::extract(json, TEST_DOMAIN);
50 | let not_extracted = AlienVault::extract(Value::Null, TEST_DOMAIN);
51 |
52 | assert!(extracted.is_ok());
53 | assert!(not_extracted.is_err());
54 |
55 | assert_eq!(extracted.unwrap(), [TEST_BAR_SUBDOMAIN.into()].into());
56 | assert_eq!(not_extracted.err().unwrap(), JSONExtract.into());
57 | }
58 |
--------------------------------------------------------------------------------
/tests/components/modules/integrations/threatcrowd_test.rs:
--------------------------------------------------------------------------------
1 | use serde_json::Value;
2 | use subscan::{
3 | enums::content::Content,
4 | error::ModuleErrorKind::JSONExtract,
5 | modules::integrations::threatcrowd::{ThreatCrowd, THREATCROWD_URL},
6 | types::result::status::SubscanModuleStatus,
7 | };
8 |
9 | use crate::common::{
10 | constants::{TEST_BAR_SUBDOMAIN, TEST_DOMAIN, TEST_URL},
11 | mock::funcs,
12 | utils,
13 | };
14 |
15 | #[tokio::test]
16 | #[stubr::mock("module/integrations/threatcrowd.json")]
17 | async fn run_test() {
18 | let mut threatcrowd = ThreatCrowd::dispatcher();
19 |
20 | funcs::wrap_module_url(&mut threatcrowd, &stubr.path("/threatcrowd"));
21 |
22 | let (results, status) = utils::run_module(threatcrowd, TEST_DOMAIN).await;
23 |
24 | assert_eq!(results, [TEST_BAR_SUBDOMAIN.into()].into());
25 | assert_eq!(status, SubscanModuleStatus::Finished);
26 | }
27 |
28 | #[tokio::test]
29 | async fn get_query_url_test() {
30 | let url = ThreatCrowd::get_query_url(TEST_DOMAIN);
31 | let expected = format!("{THREATCROWD_URL}/?domain={TEST_DOMAIN}");
32 |
33 | assert_eq!(url, expected);
34 | }
35 |
36 | #[tokio::test]
37 | async fn get_next_url_test() {
38 | let url = TEST_URL.parse().unwrap();
39 | let next = ThreatCrowd::get_next_url(url, Content::Empty);
40 |
41 | assert!(next.is_none());
42 | }
43 |
44 | #[tokio::test]
45 | async fn extract_test() {
46 | let stub = "module/integrations/threatcrowd.json";
47 | let json = utils::read_stub(stub)["response"]["jsonBody"].clone();
48 |
49 | let extracted = ThreatCrowd::extract(json, TEST_DOMAIN);
50 | let not_extracted = ThreatCrowd::extract(Value::Null, TEST_DOMAIN);
51 |
52 | assert!(extracted.is_ok());
53 | assert!(not_extracted.is_err());
54 |
55 | assert_eq!(extracted.unwrap(), [TEST_BAR_SUBDOMAIN.into()].into());
56 | assert_eq!(not_extracted.err().unwrap(), JSONExtract.into());
57 | }
58 |
--------------------------------------------------------------------------------
/src/cli/commands/module/run.rs:
--------------------------------------------------------------------------------
1 | use std::path::PathBuf;
2 |
3 | use clap::Args;
4 |
5 | use crate::{
6 | constants::{
7 | DEFAULT_HTTP_TIMEOUT, DEFAULT_RESOLVER_CONCURRENCY, DEFAULT_RESOLVER_TIMEOUT,
8 | DEFAULT_USER_AGENT,
9 | },
10 | enums::output::OutputFormat,
11 | };
12 |
13 | /// Run command to start any module
14 | #[derive(Args, Clone, Debug, Default)]
15 | pub struct ModuleRunSubCommandArgs {
16 | /// Module name to be run
17 | pub name: String,
18 | /// Target domain address to be enumerated
19 | #[arg(short, long)]
20 | pub domain: String,
21 | /// Set output format
22 | #[arg(value_enum, short, long, default_value_t = OutputFormat::JSON)]
23 | pub output: OutputFormat,
24 | /// If sets, output will be logged on stdout
25 | #[arg(long, default_value_t = false)]
26 | pub print: bool,
27 | /// Set User-Agent header value for HTTP requests
28 | #[arg(short, long, default_value = DEFAULT_USER_AGENT)]
29 | pub user_agent: String,
30 | /// HTTP timeout value as a seconds
31 | #[arg(short = 't', long, default_value_t = DEFAULT_HTTP_TIMEOUT.as_secs())]
32 | pub http_timeout: u64,
33 | /// Set HTTP proxy
34 | #[arg(short, long, default_value = None)]
35 | pub proxy: Option,
36 | /// IP resolver timeout value as a milliseconds
37 | #[arg(long, default_value_t = DEFAULT_RESOLVER_TIMEOUT.as_millis() as u64)]
38 | pub resolver_timeout: u64,
39 | /// IP resolver concurrency level, thread counts of resolver instances
40 | #[arg(long, default_value_t = DEFAULT_RESOLVER_CONCURRENCY)]
41 | pub resolver_concurrency: u64,
42 | /// A text file containing list of resolvers. See `resolverlist.template`
43 | #[arg(long, default_value = None)]
44 | pub resolver_list: Option,
45 | /// Disable IP address resolve process
46 | #[arg(long = "disable-ip-resolve", default_value_t = false)]
47 | pub resolver_disabled: bool,
48 | }
49 |
--------------------------------------------------------------------------------
/src/types/filters.rs:
--------------------------------------------------------------------------------
1 | use derive_more::From;
2 |
3 | /// This filter allows to filter modules by their names
4 | #[derive(Clone, Debug, From, PartialEq)]
5 | #[from((Vec, Vec))]
6 | pub struct ModuleNameFilter {
7 | /// Valid [`SubscanModule`](crate::types::core::SubscanModule) names list
8 | pub modules: Vec,
9 | /// Invalid [`SubscanModule`](crate::types::core::SubscanModule) names list
10 | pub skips: Vec,
11 | }
12 |
13 | impl ModuleNameFilter {
14 | /// Check module name is filtered or non-filtered by this filter
15 | ///
16 | /// # Examples
17 | ///
18 | /// ```
19 | /// use subscan::types::filters::ModuleNameFilter;
20 | ///
21 | /// #[tokio::main]
22 | /// async fn main() {
23 | /// let filter: ModuleNameFilter = (vec![], vec![]).into();
24 | /// assert!(!filter.is_filtered("foo").await);
25 | ///
26 | /// let filter: ModuleNameFilter = (vec![], vec!["foo".into()]).into();
27 | /// assert!(filter.is_filtered("foo").await);
28 | ///
29 | /// let filter: ModuleNameFilter = (vec!["bar".into()], vec![]).into();
30 | /// assert!(filter.is_filtered("foo").await);
31 | ///
32 | /// let filter: ModuleNameFilter = (vec!["foo".into()], vec!["foo".into()]).into();
33 | /// assert!(filter.is_filtered("foo").await);
34 | /// }
35 | /// ```
36 | pub async fn is_filtered(&self, name: &str) -> bool {
37 | if self.modules.is_empty() && self.skips.is_empty() {
38 | false
39 | } else if self.modules.is_empty() && !self.skips.is_empty() {
40 | self.skips.contains(&name.to_lowercase())
41 | } else if !self.modules.is_empty() && self.skips.is_empty() {
42 | !self.modules.contains(&name.to_lowercase())
43 | } else {
44 | !self.modules.contains(&name.to_lowercase())
45 | || self.skips.contains(&name.to_lowercase())
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/tests/components/modules/integrations/subdomaincenter_test.rs:
--------------------------------------------------------------------------------
1 | use serde_json::Value;
2 | use subscan::{
3 | enums::content::Content,
4 | error::ModuleErrorKind::JSONExtract,
5 | modules::integrations::subdomaincenter::{SubdomainCenter, SUBDOMAINCENTER_URL},
6 | types::result::status::SubscanModuleStatus,
7 | };
8 |
9 | use crate::common::{
10 | constants::{TEST_BAR_SUBDOMAIN, TEST_DOMAIN, TEST_URL},
11 | mock::funcs,
12 | utils,
13 | };
14 |
15 | #[tokio::test]
16 | #[stubr::mock("module/integrations/subdomaincenter.json")]
17 | async fn run_test() {
18 | let mut subdomaincenter = SubdomainCenter::dispatcher();
19 |
20 | funcs::wrap_module_url(&mut subdomaincenter, &stubr.path("/subdomaincenter"));
21 |
22 | let (results, status) = utils::run_module(subdomaincenter, TEST_DOMAIN).await;
23 |
24 | assert_eq!(results, [TEST_BAR_SUBDOMAIN.into()].into());
25 | assert_eq!(status, SubscanModuleStatus::Finished);
26 | }
27 |
28 | #[tokio::test]
29 | async fn get_query_url_test() {
30 | let url = SubdomainCenter::get_query_url(TEST_DOMAIN);
31 | let expected = format!("{SUBDOMAINCENTER_URL}/?domain={TEST_DOMAIN}");
32 |
33 | assert_eq!(url, expected);
34 | }
35 |
36 | #[tokio::test]
37 | async fn get_next_url_test() {
38 | let url = TEST_URL.parse().unwrap();
39 | let next = SubdomainCenter::get_next_url(url, Content::Empty);
40 |
41 | assert!(next.is_none());
42 | }
43 |
44 | #[tokio::test]
45 | async fn extract_test() {
46 | let stub = "module/integrations/subdomaincenter.json";
47 | let json = utils::read_stub(stub)["response"]["jsonBody"].clone();
48 |
49 | let extracted = SubdomainCenter::extract(json, TEST_DOMAIN);
50 | let not_extracted = SubdomainCenter::extract(Value::Null, TEST_DOMAIN);
51 |
52 | assert!(extracted.is_ok());
53 | assert!(not_extracted.is_err());
54 |
55 | assert_eq!(extracted.unwrap(), [TEST_BAR_SUBDOMAIN.into()].into());
56 | assert_eq!(not_extracted.err().unwrap(), JSONExtract.into());
57 | }
58 |
--------------------------------------------------------------------------------
/src/modules/engines/bing.rs:
--------------------------------------------------------------------------------
1 | use reqwest::Url;
2 |
3 | use crate::{
4 | enums::dispatchers::{RequesterDispatcher, SubscanModuleDispatcher},
5 | extractors::html::HTMLExtractor,
6 | modules::generics::engine::GenericSearchEngineModule,
7 | requesters::client::HTTPClient,
8 | types::core::SubscanModuleCoreComponents,
9 | };
10 |
11 | pub const BING_MODULE_NAME: &str = "bing";
12 | pub const BING_SEARCH_URL: &str = "https://www.bing.com/search";
13 | pub const BING_SEARCH_PARAM: &str = "q";
14 | pub const BING_CITE_TAG: &str = "cite";
15 |
16 | /// Bing search engine enumerator
17 | ///
18 | /// It uses [`GenericSearchEngineModule`] its own inner
19 | /// here are the configurations
20 | ///
21 | /// | Property | Value |
22 | /// |:------------------:|:-----------------------------:|
23 | /// | Module Name | `bing` |
24 | /// | Search URL | |
25 | /// | Search Param | `q` |
26 | /// | Subdomain Selector | `cite` |
27 | /// | Requester | [`HTTPClient`] |
28 | /// | Extractor | [`HTMLExtractor`] |
29 | /// | Generic | [`GenericSearchEngineModule`] |
30 | pub struct Bing {}
31 |
32 | impl Bing {
33 | pub fn dispatcher() -> SubscanModuleDispatcher {
34 | let url = Url::parse(BING_SEARCH_URL);
35 |
36 | let extractor: HTMLExtractor = HTMLExtractor::new(BING_CITE_TAG.into(), vec![]);
37 | let requester: RequesterDispatcher = HTTPClient::default().into();
38 |
39 | let generic = GenericSearchEngineModule {
40 | name: BING_MODULE_NAME.into(),
41 | param: BING_SEARCH_PARAM.into(),
42 | url: url.unwrap(),
43 | components: SubscanModuleCoreComponents {
44 | requester: requester.into(),
45 | extractor: extractor.into(),
46 | },
47 | };
48 |
49 | generic.into()
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/modules/engines/google.rs:
--------------------------------------------------------------------------------
1 | use reqwest::Url;
2 |
3 | use crate::{
4 | enums::dispatchers::{RequesterDispatcher, SubscanModuleDispatcher},
5 | extractors::html::HTMLExtractor,
6 | modules::generics::engine::GenericSearchEngineModule,
7 | requesters::client::HTTPClient,
8 | types::core::SubscanModuleCoreComponents,
9 | };
10 |
11 | pub const GOOGLE_MODULE_NAME: &str = "google";
12 | pub const GOOGLE_SEARCH_URL: &str = "https://www.google.com/search";
13 | pub const GOOGLE_SEARCH_PARAM: &str = "q";
14 | pub const GOOGLE_CITE_TAG: &str = "cite";
15 |
16 | /// Google search engine enumerator
17 | ///
18 | /// It uses [`GenericSearchEngineModule`] its own inner
19 | /// here are the configurations
20 | ///
21 | /// | Property | Value |
22 | /// |:------------------:|:-------------------------------:|
23 | /// | Module Name | `google` |
24 | /// | Search URL | |
25 | /// | Search Param | `q` |
26 | /// | Subdomain Selector | `cite` |
27 | /// | Requester | [`HTTPClient`] |
28 | /// | Extractor | [`HTMLExtractor`] |
29 | /// | Generic | [`GenericSearchEngineModule`] |
30 | pub struct Google {}
31 |
32 | impl Google {
33 | pub fn dispatcher() -> SubscanModuleDispatcher {
34 | let url = Url::parse(GOOGLE_SEARCH_URL);
35 |
36 | let extractor: HTMLExtractor = HTMLExtractor::new(GOOGLE_CITE_TAG.into(), vec![]);
37 | let requester: RequesterDispatcher = HTTPClient::default().into();
38 |
39 | let generic = GenericSearchEngineModule {
40 | name: GOOGLE_MODULE_NAME.into(),
41 | param: GOOGLE_SEARCH_PARAM.into(),
42 | url: url.unwrap(),
43 | components: SubscanModuleCoreComponents {
44 | requester: requester.into(),
45 | extractor: extractor.into(),
46 | },
47 | };
48 |
49 | generic.into()
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/tests/components/modules/integrations/chaos_test.rs:
--------------------------------------------------------------------------------
1 | use std::env;
2 |
3 | use serde_json::Value;
4 | use subscan::{
5 | enums::content::Content,
6 | error::ModuleErrorKind::JSONExtract,
7 | interfaces::module::SubscanModuleInterface,
8 | modules::integrations::chaos::{Chaos, CHAOS_URL},
9 | types::result::status::SubscanModuleStatus,
10 | };
11 |
12 | use crate::common::{
13 | constants::{TEST_BAR_SUBDOMAIN, TEST_DOMAIN, TEST_URL},
14 | mock::funcs,
15 | utils,
16 | };
17 |
18 | #[tokio::test]
19 | #[stubr::mock("module/integrations/chaos.json")]
20 | async fn run_test() {
21 | let mut chaos = Chaos::dispatcher();
22 | let env_name = chaos.envs().await.apikey.name;
23 |
24 | env::set_var(&env_name, "chaos-api-key");
25 | funcs::wrap_module_url(&mut chaos, &stubr.path("/chaos"));
26 |
27 | let (results, status) = utils::run_module(chaos, TEST_DOMAIN).await;
28 |
29 | assert_eq!(results, [TEST_BAR_SUBDOMAIN.into()].into());
30 | assert_eq!(status, SubscanModuleStatus::Finished);
31 |
32 | env::remove_var(env_name);
33 | }
34 |
35 | #[tokio::test]
36 | async fn get_query_url_test() {
37 | let url = Chaos::get_query_url(TEST_DOMAIN);
38 | let expected = format!("{CHAOS_URL}/{TEST_DOMAIN}/subdomains");
39 |
40 | assert_eq!(url, expected);
41 | }
42 |
43 | #[tokio::test]
44 | async fn get_next_url_test() {
45 | let url = TEST_URL.parse().unwrap();
46 | let next = Chaos::get_next_url(url, Content::Empty);
47 |
48 | assert!(next.is_none());
49 | }
50 |
51 | #[tokio::test]
52 | async fn extract_test() {
53 | let stub = "module/integrations/chaos.json";
54 | let json = utils::read_stub(stub)["response"]["jsonBody"].clone();
55 |
56 | let extracted = Chaos::extract(json, TEST_DOMAIN);
57 | let not_extracted = Chaos::extract(Value::Null, TEST_DOMAIN);
58 |
59 | assert!(extracted.is_ok());
60 | assert!(not_extracted.is_err());
61 |
62 | assert_eq!(extracted.unwrap(), [TEST_BAR_SUBDOMAIN.into()].into());
63 | assert_eq!(not_extracted.err().unwrap(), JSONExtract.into());
64 | }
65 |
--------------------------------------------------------------------------------
/book/src/user-guide/commands/brute.md:
--------------------------------------------------------------------------------
1 | # `brute`
2 |
3 | With this command you can use the brute force technique to discover subdomains on a domain
4 |
5 | ## Argument List
6 |
7 | All arguments below can be used with the `brute` command, [see here](#common-use-cases) for common use cases
8 |
9 | | Name | Short | Description |
10 | | :----------------------- | :---: | :----------------------------------------------: |
11 | | `--domain` | `-d` | Target domain to be scanned |
12 | | `--wordlist` | `-w` | Wordlist file to be used during attack |
13 | | `--print` | | If sets, output will be logged on stdout |
14 | | `--stream-to-txt` | `-s` | Optional `txt` file to create file stream for the subdomains that found. If sets the `--output` parameter will be disabled |
15 | | `--output` | `-o` | Set output format (`txt`, `csv`, `json`, `html`) |
16 | | `--resolver-timeout` | | IP resolver timeout |
17 | | `--resolver-concurrency` | | IP resolver concurrency level |
18 | | `--resolver-list` | | A text file containing list of resolvers. See `resolverlist.template` |
19 | | `--help` | `-h` | Print help |
20 |
21 | ## Common Use Cases
22 |
23 | - Run a basic brute force attack with default settings
24 |
25 | ```bash
26 | ~$ subscan brute -d example.com -w wordlist.txt
27 | ```
28 |
29 | - Increase resolver concurrency to improve attack speed
30 |
31 | ```bash
32 | ~$ subscan brute -d example.com -w wordlist.txt --resolver-concurrency 200
33 | ```
34 |
35 | - Fine-tune IP address resolver component according to your network
36 |
37 | ```bash
38 | ~$ subscan brute -d example.com -w wordlist.txt --resolver-timeout 1 --resolver-concurrency 100
39 | ```
40 |
41 | - Skip creating a report and print results directly to `stdout`
42 |
43 | ```bash
44 | ~$ subscan brute -d example.com -w wordlist.txt --print
45 | ```
46 |
--------------------------------------------------------------------------------
/tests/components/modules/integrations/bevigil_test.rs:
--------------------------------------------------------------------------------
1 | use std::env;
2 |
3 | use serde_json::Value;
4 | use subscan::{
5 | enums::content::Content,
6 | error::ModuleErrorKind::JSONExtract,
7 | interfaces::module::SubscanModuleInterface,
8 | modules::integrations::bevigil::{Bevigil, BEVIGIL_URL},
9 | types::result::status::SubscanModuleStatus,
10 | };
11 |
12 | use crate::common::{
13 | constants::{TEST_BAR_SUBDOMAIN, TEST_DOMAIN, TEST_URL},
14 | mock::funcs,
15 | utils,
16 | };
17 |
18 | #[tokio::test]
19 | #[stubr::mock("module/integrations/bevigil.json")]
20 | async fn run_test() {
21 | let mut bevigil = Bevigil::dispatcher();
22 | let env_name = bevigil.envs().await.apikey.name;
23 |
24 | env::set_var(&env_name, "bevigil-api-key");
25 | funcs::wrap_module_url(&mut bevigil, &stubr.path("/bevigil"));
26 |
27 | let (results, status) = utils::run_module(bevigil, TEST_DOMAIN).await;
28 |
29 | assert_eq!(results, [TEST_BAR_SUBDOMAIN.into()].into());
30 | assert_eq!(status, SubscanModuleStatus::Finished);
31 |
32 | env::remove_var(env_name);
33 | }
34 |
35 | #[tokio::test]
36 | async fn get_query_url_test() {
37 | let url = Bevigil::get_query_url(TEST_DOMAIN);
38 | let expected = format!("{BEVIGIL_URL}/{TEST_DOMAIN}/subdomains");
39 |
40 | assert_eq!(url, expected);
41 | }
42 |
43 | #[tokio::test]
44 | async fn get_next_url_test() {
45 | let url = TEST_URL.parse().unwrap();
46 | let next = Bevigil::get_next_url(url, Content::Empty);
47 |
48 | assert!(next.is_none());
49 | }
50 |
51 | #[tokio::test]
52 | async fn extract_test() {
53 | let stub = "module/integrations/bevigil.json";
54 | let json = utils::read_stub(stub)["response"]["jsonBody"].clone();
55 |
56 | let extracted = Bevigil::extract(json, TEST_DOMAIN);
57 | let not_extracted = Bevigil::extract(Value::Null, TEST_DOMAIN);
58 |
59 | assert!(extracted.is_ok());
60 | assert!(not_extracted.is_err());
61 |
62 | assert_eq!(extracted.unwrap(), [TEST_BAR_SUBDOMAIN.into()].into());
63 | assert_eq!(not_extracted.err().unwrap(), JSONExtract.into());
64 | }
65 |
--------------------------------------------------------------------------------
/tests/components/modules/integrations/netcraft_test.rs:
--------------------------------------------------------------------------------
1 | use reqwest::Url;
2 | use subscan::{
3 | enums::dispatchers::SubscanModuleDispatcher,
4 | modules::integrations::netcraft::{Netcraft, NETCRAFT_URL},
5 | requesters::client::HTTPClient,
6 | types::result::status::SubscanModuleStatus,
7 | };
8 | use tokio::sync::Mutex;
9 |
10 | use crate::common::{
11 | constants::{TEST_BAR_SUBDOMAIN, TEST_DOMAIN, TEST_URL},
12 | mock::funcs,
13 | utils,
14 | };
15 |
16 | #[tokio::test]
17 | #[stubr::mock("module/integrations/netcraft.json")]
18 | async fn run_test() {
19 | let mut netcraft = Netcraft::dispatcher();
20 | let new_requester = HTTPClient::default();
21 |
22 | funcs::wrap_module_url(&mut netcraft, &stubr.path("/netcraft"));
23 |
24 | if let SubscanModuleDispatcher::GenericIntegrationModule(ref mut netcraft) = netcraft {
25 | netcraft.components.requester = Mutex::new(new_requester.into());
26 | }
27 |
28 | let (results, status) = utils::run_module(netcraft, TEST_DOMAIN).await;
29 |
30 | assert_eq!(results, [TEST_BAR_SUBDOMAIN.into()].into());
31 | assert_eq!(status, SubscanModuleStatus::Finished);
32 | }
33 |
34 | #[tokio::test]
35 | async fn get_query_url_test() {
36 | let params = &[("restriction", "site+ends+with"), ("host", TEST_DOMAIN)];
37 |
38 | let expected = Url::parse_with_params(NETCRAFT_URL, params).unwrap();
39 | let url = Netcraft::get_query_url(TEST_DOMAIN);
40 |
41 | assert_eq!(url, expected.to_string());
42 | }
43 |
44 | #[tokio::test]
45 | async fn get_next_url_test() {
46 | let html = "
";
47 |
48 | let url = TEST_URL.parse().unwrap();
49 | let next = Netcraft::get_next_url(url, html.into());
50 | let expected = format!("{NETCRAFT_URL}/page-2");
51 |
52 | assert_eq!(next.unwrap().to_string(), expected);
53 | }
54 |
55 | #[tokio::test]
56 | async fn get_next_url_fail_test() {
57 | let html = "
";
58 |
59 | let url = TEST_URL.parse().unwrap();
60 | let next = Netcraft::get_next_url(url, html.into());
61 |
62 | assert!(next.is_none());
63 | }
64 |
--------------------------------------------------------------------------------
/tests/components/modules/integrations/bufferover_test.rs:
--------------------------------------------------------------------------------
1 | use std::env;
2 |
3 | use serde_json::Value;
4 | use subscan::{
5 | enums::content::Content,
6 | error::ModuleErrorKind::JSONExtract,
7 | interfaces::module::SubscanModuleInterface,
8 | modules::integrations::bufferover::{BufferOver, BUFFEROVER_URL},
9 | types::result::status::SubscanModuleStatus,
10 | };
11 |
12 | use crate::common::{
13 | constants::{TEST_BAR_SUBDOMAIN, TEST_DOMAIN, TEST_URL},
14 | mock::funcs,
15 | utils,
16 | };
17 |
18 | #[tokio::test]
19 | #[stubr::mock("module/integrations/bufferover.json")]
20 | async fn run_test() {
21 | let mut bufferover = BufferOver::dispatcher();
22 | let env_name = bufferover.envs().await.apikey.name;
23 |
24 | env::set_var(&env_name, "bufferover-api-key");
25 | funcs::wrap_module_url(&mut bufferover, &stubr.path("/bufferover"));
26 |
27 | let (results, status) = utils::run_module(bufferover, TEST_DOMAIN).await;
28 |
29 | assert_eq!(results, [TEST_BAR_SUBDOMAIN.into()].into());
30 | assert_eq!(status, SubscanModuleStatus::Finished);
31 |
32 | env::remove_var(env_name);
33 | }
34 |
35 | #[tokio::test]
36 | async fn get_query_url_test() {
37 | let url = BufferOver::get_query_url(TEST_DOMAIN);
38 | let expected = format!("{BUFFEROVER_URL}/dns?q={TEST_DOMAIN}");
39 |
40 | assert_eq!(url, expected);
41 | }
42 |
43 | #[tokio::test]
44 | async fn get_next_url_test() {
45 | let url = TEST_URL.parse().unwrap();
46 | let next = BufferOver::get_next_url(url, Content::Empty);
47 |
48 | assert!(next.is_none());
49 | }
50 |
51 | #[tokio::test]
52 | async fn extract_test() {
53 | let stub = "module/integrations/bufferover.json";
54 | let json = utils::read_stub(stub)["response"]["jsonBody"].clone();
55 |
56 | let extracted = BufferOver::extract(json, TEST_DOMAIN);
57 | let not_extracted = BufferOver::extract(Value::Null, TEST_DOMAIN);
58 |
59 | assert!(extracted.is_ok());
60 | assert!(not_extracted.is_err());
61 |
62 | assert_eq!(extracted.unwrap(), [TEST_BAR_SUBDOMAIN.into()].into());
63 | assert_eq!(not_extracted.err().unwrap(), JSONExtract.into());
64 | }
65 |
--------------------------------------------------------------------------------
/tests/components/modules/integrations/whoisxmlapi_test.rs:
--------------------------------------------------------------------------------
1 | use std::env;
2 |
3 | use serde_json::Value;
4 | use subscan::{
5 | enums::content::Content,
6 | error::ModuleErrorKind::JSONExtract,
7 | interfaces::module::SubscanModuleInterface,
8 | modules::integrations::whoisxmlapi::{WhoisXMLAPI, WHOISXMLAPI_URL},
9 | types::result::status::SubscanModuleStatus,
10 | };
11 |
12 | use crate::common::{
13 | constants::{TEST_BAR_SUBDOMAIN, TEST_DOMAIN, TEST_URL},
14 | mock::funcs,
15 | utils,
16 | };
17 |
18 | #[tokio::test]
19 | #[stubr::mock("module/integrations/whoisxmlapi.json")]
20 | async fn run_test() {
21 | let mut whoisxmlapi = WhoisXMLAPI::dispatcher();
22 | let env_name = whoisxmlapi.envs().await.apikey.name;
23 |
24 | env::set_var(&env_name, "whoisxmlapi-api-key");
25 | funcs::wrap_module_url(&mut whoisxmlapi, &stubr.path("/whoisxmlapi"));
26 |
27 | let (results, status) = utils::run_module(whoisxmlapi, TEST_DOMAIN).await;
28 |
29 | assert_eq!(results, [TEST_BAR_SUBDOMAIN.into()].into());
30 | assert_eq!(status, SubscanModuleStatus::Finished);
31 |
32 | env::remove_var(env_name);
33 | }
34 |
35 | #[tokio::test]
36 | async fn get_query_url_test() {
37 | let url = WhoisXMLAPI::get_query_url(TEST_DOMAIN);
38 | let expected = format!("{WHOISXMLAPI_URL}/?domainName={TEST_DOMAIN}");
39 |
40 | assert_eq!(url, expected);
41 | }
42 |
43 | #[tokio::test]
44 | async fn get_next_url_test() {
45 | let url = TEST_URL.parse().unwrap();
46 | let next = WhoisXMLAPI::get_next_url(url, Content::Empty);
47 |
48 | assert!(next.is_none());
49 | }
50 |
51 | #[tokio::test]
52 | async fn extract_test() {
53 | let stub = "module/integrations/whoisxmlapi.json";
54 | let json = utils::read_stub(stub)["response"]["jsonBody"].clone();
55 |
56 | let extracted = WhoisXMLAPI::extract(json, TEST_DOMAIN);
57 | let not_extracted = WhoisXMLAPI::extract(Value::Null, TEST_DOMAIN);
58 |
59 | assert!(extracted.is_ok());
60 | assert!(not_extracted.is_err());
61 |
62 | assert_eq!(extracted.unwrap(), [TEST_BAR_SUBDOMAIN.into()].into());
63 | assert_eq!(not_extracted.err().unwrap(), JSONExtract.into());
64 | }
65 |
--------------------------------------------------------------------------------
/src/constants.rs:
--------------------------------------------------------------------------------
1 | use std::time::Duration;
2 |
3 | /// `Subscan` banner module path
4 | pub const SUBSCAN_BANNER_LOG_TARGET: &str = "subscan::banner";
5 | /// `Subscan` environment variable namespace
6 | pub const SUBSCAN_ENV_NAMESPACE: &str = "SUBSCAN";
7 | /// `Subscan` Chrome browser executable path env
8 | pub const SUBSCAN_CHROME_PATH_ENV: &str = "SUBSCAN_CHROME_PATH";
9 | /// Concurrency level of module runner instances, count of threads
10 | pub const DEFAULT_MODULE_CONCURRENCY: u64 = 5;
11 | /// Concurrency level of resolver instances, count of threads
12 | pub const DEFAULT_RESOLVER_CONCURRENCY: u64 = 100;
13 | /// Default HTTP timeout as a [`Duration`]
14 | pub const DEFAULT_HTTP_TIMEOUT: Duration = Duration::from_secs(30);
15 | /// Default DNS resolver timeout as a [`Duration`]
16 | pub const DEFAULT_RESOLVER_TIMEOUT: Duration = Duration::from_millis(1000);
17 | /// Default User-Agent headers for HTTP requests
18 | pub const DEFAULT_USER_AGENT: &str = "\
19 | Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) \
20 | AppleWebKit/537.36 (KHTML, like Gecko) \
21 | Chrome/135.0.0.0 Safari/537.36\
22 | ";
23 | /// Asterisk character to indicate all modules
24 | pub const ASTERISK: &str = "*";
25 | /// Time logging format
26 | pub const LOG_TIME_FORMAT: &str = "%H:%M:%S %Z";
27 | /// Port number pattern to parse resolver ports from file
28 | pub const RL_PORT_PATTERN: &str = r#"(?([1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5]))"#;
29 | /// IPv4 pattern to find IPv4 addresses from resolver list file. This regex
30 | /// pattern is used to parse IPv4 addresses and perform basic validations.
31 | /// It is used to make sure that IPv4 addresses are correctly written to
32 | /// the resolver list file in valid format
33 | pub const RL_IPV4_PATTERN: &str = r#"(?(\b25[0-5]|\b2[0-4][0-9]|\b[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3})"#;
34 | /// IPv6 pattern to find IPv6 addresses from resolver list file. This regex
35 | /// pattern does not validate the IPv6 address, it only checks if it is
36 | /// written to the file in the valid format and is used to parse the IPv6 address
37 | pub const RL_IPV6_PATTERN: &str = r#"\[(?.+)\]"#;
38 |
--------------------------------------------------------------------------------
/book/src/development/environment.md:
--------------------------------------------------------------------------------
1 | # Setup Development Environment
2 |
3 | This section covers topics like setting up a development environment and running tests for those who want to contribute to `Subscan`
4 |
5 | To set up your development environment, please follow the instructions below
6 |
7 | 1. Clone repository
8 |
9 | ```bash
10 | ~$ git clone https://github.com/eredotpkfr/subscan && cd subscan
11 | ```
12 |
13 | 2. Install `pre-commit` and its hooks
14 |
15 | ```bash
16 | ~$ # Install pre-commit Mac or Linux
17 | ~$ make install-pre-commit-mac
18 | ~$ # Install pre-commit hooks
19 | ~$ make install-pre-commit-hooks
20 | ~$ # Check everything is OK
21 | ~$ pre-commit run -a
22 | ```
23 |
24 | 3. Install required cargo tools for development
25 |
26 | ```bash
27 | ~$ # Install cargo tools
28 | ~$ make install-cargo-tools
29 | ```
30 |
31 | 4. Create `.env` file from `.env.template`
32 |
33 | ```bash
34 | ~$ cp .env.template .env
35 | ```
36 |
37 | 5. Finally build the project and run CLI
38 |
39 | ```bash
40 | ~$ cargo build && target/debug/subscan --help
41 | ```
42 |
43 | ## Running Tests
44 |
45 | You have many options to run the tests, below are the command sets on how to run the tests differently
46 |
47 | ```bash
48 | ~$ # run all tests
49 | ~$ cargo test # or `make test`
50 | ~$ # capture outputs
51 | ~$ cargo test -- --nocapture
52 | ~$ # run only doc tests
53 | ~$ cargo test --doc
54 | ~$ # run a single test
55 | ~$ cargo test -- engines::bing_test::bing_run_test
56 | ~$ # run only integration tests
57 | ~$ cargo test --tests modules::integrations
58 | ```
59 |
60 | To run tests via [nextest](https://nexte.st/), run following command
61 |
62 | ```bash
63 | ~$ make nextest
64 | ```
65 |
66 | Create coverage report with [cargo-llvm-cov](https://github.com/taiki-e/cargo-llvm-cov)
67 |
68 | ```bash
69 | ~$ make coverage
70 | ```
71 |
72 | ## Building Docs
73 |
74 | To build documentations, run following command
75 |
76 | ```bash
77 | ~$ make doc # or `cargo doc`
78 | ```
79 |
80 | To serve project book with hot reload, use following command
81 |
82 | ```bash
83 | ~$ # run book tests and serve
84 | ~$ make live-book
85 | ```
86 |
--------------------------------------------------------------------------------
/book/src/user-guide/environments.md:
--------------------------------------------------------------------------------
1 | # Environments
2 |
3 | `Subscan` has the ability to read all your environment variables from the `.env` file in your working directory. To learn how to define your environment variables in the `.env` file, you can refer to the `.env.template` file. All the `Subscan` environment variables uses `SUBSCAN` namespace as a prefix
4 |
5 | There are two types of environment variables:
6 |
7 | - **Dynamic:** These environment variables follow a specific format (e.g., `SUBSCAN__FOO`) and `Subscan` can read them automatically
8 | - **Static:** These are predefined environment variables that we know already
9 |
10 | ## Statics
11 |
12 |
13 |
14 | | Name | Required | Description |
15 | | :----------------------------- | :------: | :---------: |
16 | | `SUBSCAN_CHROME_PATH` | `false` | Specify your Chrome executable. If not specified, the Chrome binary will be fetched automatically by headless_chrome based on your system architecture |
17 |
18 |
19 |
20 | ## Dynamics
21 |
22 | | Name | Required | Description |
23 | | :----------------------------- | :------: | :---------: |
24 | | `SUBSCAN__HOST` | `false` | Some API integration modules can provide user specific host, for these cases, set module specific host |
25 | | `SUBSCAN__APIKEY` | `false` | Some modules may include API integration and require an API key for authentication. Set the API key in these cases |
26 | | `SUBSCAN__USERNAME` | `false` | Set the username for a module if it uses HTTP basic authentication |
27 | | `SUBSCAN__PASSWORD` | `false` | Set the password for a module if it uses HTTP basic authentication |
28 |
29 | ## Creating `.env` File
30 |
31 | Please see the [.env.template](https://github.com/eredotpkfr/subscan/blob/main/.env.template) file in project repository. Your `.env` file should follow a similar format as shown below
32 |
33 | ```bash
34 | SUBSCAN_BEVIGIL_APIKEY=foo
35 | SUBSCAN_BINARYEDGE_APIKEY=bar
36 | SUBSCAN_BUFFEROVER_APIKEY=baz
37 | ```
38 |
--------------------------------------------------------------------------------
/tests/components/modules/integrations/dnsdumpsterapi_test.rs:
--------------------------------------------------------------------------------
1 | use std::env;
2 |
3 | use serde_json::Value;
4 | use subscan::{
5 | enums::content::Content,
6 | error::ModuleErrorKind::JSONExtract,
7 | interfaces::module::SubscanModuleInterface,
8 | modules::integrations::dnsdumpsterapi::{DNSDumpsterAPI, DNSDUMPSTERAPI_URL},
9 | types::result::status::SubscanModuleStatus,
10 | };
11 |
12 | use crate::common::{
13 | constants::{TEST_BAR_SUBDOMAIN, TEST_DOMAIN, TEST_URL},
14 | mock::funcs,
15 | utils,
16 | };
17 |
18 | #[tokio::test]
19 | #[stubr::mock("module/integrations/dnsdumpsterapi.json")]
20 | async fn run_test() {
21 | let mut dnsdumpsterapi = DNSDumpsterAPI::dispatcher();
22 | let env_name = dnsdumpsterapi.envs().await.apikey.name;
23 |
24 | env::set_var(&env_name, "dnsdumpsterapi-api-key");
25 | funcs::wrap_module_url(&mut dnsdumpsterapi, &stubr.path("/dnsdumpsterapi"));
26 |
27 | let (results, status) = utils::run_module(dnsdumpsterapi, TEST_DOMAIN).await;
28 |
29 | assert_eq!(results, [TEST_BAR_SUBDOMAIN.into()].into());
30 | assert_eq!(status, SubscanModuleStatus::Finished);
31 |
32 | env::remove_var(env_name);
33 | }
34 |
35 | #[tokio::test]
36 | async fn get_query_url_test() {
37 | let url = DNSDumpsterAPI::get_query_url(TEST_DOMAIN);
38 | let expected = format!("{DNSDUMPSTERAPI_URL}/{TEST_DOMAIN}");
39 |
40 | assert_eq!(url, expected);
41 | }
42 |
43 | #[tokio::test]
44 | async fn get_next_url_test() {
45 | let url = TEST_URL.parse().unwrap();
46 | let next = DNSDumpsterAPI::get_next_url(url, Content::Empty);
47 |
48 | assert!(next.is_none());
49 | }
50 |
51 | #[tokio::test]
52 | async fn extract_test() {
53 | let stub = "module/integrations/dnsdumpsterapi.json";
54 | let json = utils::read_stub(stub)["response"]["jsonBody"].clone();
55 |
56 | let extracted = DNSDumpsterAPI::extract(json, TEST_DOMAIN);
57 | let not_extracted = DNSDumpsterAPI::extract(Value::Null, TEST_DOMAIN);
58 |
59 | assert!(extracted.is_ok());
60 | assert!(not_extracted.is_err());
61 |
62 | assert_eq!(extracted.unwrap(), [TEST_BAR_SUBDOMAIN.into()].into());
63 | assert_eq!(not_extracted.err().unwrap(), JSONExtract.into());
64 | }
65 |
--------------------------------------------------------------------------------
/tests/components/modules/integrations/securitytrails_test.rs:
--------------------------------------------------------------------------------
1 | use std::env;
2 |
3 | use serde_json::Value;
4 | use subscan::{
5 | enums::content::Content,
6 | error::ModuleErrorKind::JSONExtract,
7 | interfaces::module::SubscanModuleInterface,
8 | modules::integrations::securitytrails::{SecurityTrails, SECURITYTRAILS_URL},
9 | types::result::status::SubscanModuleStatus,
10 | };
11 |
12 | use crate::common::{
13 | constants::{TEST_BAR_SUBDOMAIN, TEST_DOMAIN, TEST_URL},
14 | mock::funcs,
15 | utils,
16 | };
17 |
18 | #[tokio::test]
19 | #[stubr::mock("module/integrations/securitytrails.json")]
20 | async fn run_test() {
21 | let mut securitytrails = SecurityTrails::dispatcher();
22 | let env_name = securitytrails.envs().await.apikey.name;
23 |
24 | env::set_var(&env_name, "securitytrails-api-key");
25 | funcs::wrap_module_url(&mut securitytrails, &stubr.path("/securitytrails"));
26 |
27 | let (results, status) = utils::run_module(securitytrails, TEST_DOMAIN).await;
28 |
29 | assert_eq!(results, [TEST_BAR_SUBDOMAIN.into()].into());
30 | assert_eq!(status, SubscanModuleStatus::Finished);
31 |
32 | env::remove_var(env_name);
33 | }
34 |
35 | #[tokio::test]
36 | async fn get_query_url_test() {
37 | let url = SecurityTrails::get_query_url(TEST_DOMAIN);
38 | let expected = format!("{SECURITYTRAILS_URL}/{TEST_DOMAIN}/subdomains");
39 |
40 | assert_eq!(url, expected);
41 | }
42 |
43 | #[tokio::test]
44 | async fn get_next_url_test() {
45 | let url = TEST_URL.parse().unwrap();
46 | let next = SecurityTrails::get_next_url(url, Content::Empty);
47 |
48 | assert!(next.is_none());
49 | }
50 |
51 | #[tokio::test]
52 | async fn extract_test() {
53 | let stub = "module/integrations/securitytrails.json";
54 | let json = utils::read_stub(stub)["response"]["jsonBody"].clone();
55 |
56 | let extracted = SecurityTrails::extract(json, TEST_DOMAIN);
57 | let not_extracted = SecurityTrails::extract(Value::Null, TEST_DOMAIN);
58 |
59 | assert!(extracted.is_ok());
60 | assert!(not_extracted.is_err());
61 |
62 | assert_eq!(extracted.unwrap(), [TEST_BAR_SUBDOMAIN.into()].into());
63 | assert_eq!(not_extracted.err().unwrap(), JSONExtract.into());
64 | }
65 |
--------------------------------------------------------------------------------
/src/modules/integrations/mod.rs:
--------------------------------------------------------------------------------
1 | /// `AlienVault` API integration module
2 | pub mod alienvault;
3 | /// `Anubis` API integration module
4 | pub mod anubis;
5 | /// `Bevigil` API integration module, API key required
6 | pub mod bevigil;
7 | /// `BinaryEdge` API integration mmodule, API key required
8 | pub mod binaryedge;
9 | /// `BufferOver` API integration mmodule, API key required
10 | pub mod bufferover;
11 | /// `BuiltWith` API integration mmodule, API key required
12 | pub mod builtwith;
13 | /// `Censys` API integration, basic HTTP auth required but `Authorization`
14 | /// header can be used (e.g. `Authorization: Basic foo`)
15 | pub mod censys;
16 | /// `CertSpotter` API integration, API key required
17 | pub mod certspotter;
18 | /// `Chaos` API integration, API key required
19 | pub mod chaos;
20 | /// `CommonCrawl` non-generic module integration
21 | pub mod commoncrawl;
22 | /// `Crt.sh` API integration
23 | pub mod crtsh;
24 | /// `Digitorus` HTML crawler integration
25 | pub mod digitorus;
26 | /// `DNSDumpster` API integration, API key required
27 | pub mod dnsdumpsterapi;
28 | /// `DNSDumpster` non-generic integration
29 | pub mod dnsdumpstercrawler;
30 | /// `DnsRepo` html crawler integration
31 | pub mod dnsrepo;
32 | /// `GitHub` non-generic integration
33 | pub mod github;
34 | /// `HackerTarget` HTML crawler integration
35 | pub mod hackertarget;
36 | /// `Leakix` API integration
37 | pub mod leakix;
38 | /// `Netcraft` HTML crawler integration
39 | pub mod netcraft;
40 | /// `Netlas` non-generic API integration, API key required
41 | pub mod netlas;
42 | /// `SecurityTrails` API integration, API key required
43 | pub mod securitytrails;
44 | /// `Shodan` API integration, API key required
45 | pub mod shodan;
46 | /// `Sitedossier` HTML crawler integration
47 | pub mod sitedossier;
48 | /// `SubdomainCenter` API integration module
49 | pub mod subdomaincenter;
50 | /// `ThreatCrowd` API integration
51 | pub mod threatcrowd;
52 | /// `VirusTotal` API integration, API key required
53 | pub mod virustotal;
54 | /// `WaybackArchive` non-generic integration
55 | pub mod waybackarchive;
56 | /// `WhoisXMLAPI` API integration, API key required
57 | pub mod whoisxmlapi;
58 | /// `ZoomEye` API integration, API key required
59 | pub mod zoomeye;
60 |
--------------------------------------------------------------------------------
/src/modules/engines/yahoo.rs:
--------------------------------------------------------------------------------
1 | use reqwest::Url;
2 |
3 | use crate::{
4 | enums::dispatchers::{RequesterDispatcher, SubscanModuleDispatcher},
5 | extractors::html::HTMLExtractor,
6 | modules::generics::engine::GenericSearchEngineModule,
7 | requesters::client::HTTPClient,
8 | types::core::SubscanModuleCoreComponents,
9 | };
10 |
11 | pub const YAHOO_MODULE_NAME: &str = "yahoo";
12 | pub const YAHOO_SEARCH_URL: &str = "https://search.yahoo.com/search";
13 | pub const YAHOO_SEARCH_PARAM: &str = "p";
14 | pub const YAHOO_CITE_TAG: &str = "ol > li > div > div > h3 > a > span";
15 |
16 | /// Yahoo search engine enumerator
17 | ///
18 | /// It uses [`GenericSearchEngineModule`] its own inner
19 | /// here are the configurations
20 | ///
21 | /// | Property | Value |
22 | /// |:------------------:|:-------------------------------------:|
23 | /// | Module Name | `yahoo` |
24 | /// | Search URL | |
25 | /// | Search Param | `p` |
26 | /// | Subdomain Selector | `ol > li > div > div > h3 > a > span` |
27 | /// | Requester | [`HTTPClient`] |
28 | /// | Extractor | [`HTMLExtractor`] |
29 | /// | Generic | [`GenericSearchEngineModule`] |
30 | pub struct Yahoo {}
31 |
32 | impl Yahoo {
33 | pub fn dispatcher() -> SubscanModuleDispatcher {
34 | let url = Url::parse(YAHOO_SEARCH_URL);
35 |
36 | let selector: String = YAHOO_CITE_TAG.into();
37 | let removes: Vec = vec!["".into(), "".into()];
38 |
39 | let extractor: HTMLExtractor = HTMLExtractor::new(selector, removes);
40 | let requester: RequesterDispatcher = HTTPClient::default().into();
41 |
42 | let generic = GenericSearchEngineModule {
43 | name: YAHOO_MODULE_NAME.into(),
44 | param: YAHOO_SEARCH_PARAM.into(),
45 | url: url.unwrap(),
46 | components: SubscanModuleCoreComponents {
47 | requester: requester.into(),
48 | extractor: extractor.into(),
49 | },
50 | };
51 |
52 | generic.into()
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/book/src/development/generics/integration.md:
--------------------------------------------------------------------------------
1 | # Generic Integration Module
2 |
3 | The [`GenericIntegrationModule`](https://docs.rs/subscan/latest/subscan/modules/generics/integration/struct.GenericIntegrationModule.html) is primarily used for simple `API` integrations. To understand how it works, check the source code on the [docs.rs](https://docs.rs/subscan/latest/subscan/modules/generics/integration/struct.GenericIntegrationModule.html) page. Additionally, looking at the source code of other modules that use this implementation will help you understand how to utilize it
4 |
5 | A module that uses this one internally would look like the following
6 |
7 | ```rust,ignore
8 | pub const EXAMPLE_MODULE_NAME: &str = "example";
9 | pub const EXAMPLE_URL: &str = "https://api.example.com/api/v1";
10 |
11 | pub struct ExampleModule {}
12 |
13 | impl ExampleModule {
14 | pub fn dispatcher() -> SubscanModuleDispatcher {
15 | let requester: RequesterDispatcher = HTTPClient::default().into();
16 | let extractor: JSONExtractor = JSONExtractor::new(Box::new(Self::extract));
17 |
18 | let generic = GenericIntegrationModule {
19 | name: EXAMPLE_MODULE_NAME.into(),
20 | auth: AuthenticationMethod::NoAuthentication,
21 | funcs: GenericIntegrationCoreFuncs {
22 | url: Box::new(Self::get_query_url),
23 | next: Box::new(Self::get_next_url),
24 | },
25 | components: SubscanModuleCoreComponents {
26 | requester: requester.into(),
27 | extractor: extractor.into(),
28 | },
29 | };
30 |
31 | generic.into()
32 | }
33 |
34 | pub fn get_query_url(domain: &str) -> String {
35 | format!("{EXAMPLE_URL}/{domain}/subdomains")
36 | }
37 |
38 | pub fn get_next_url(_url: Url, _content: Content) -> Option {
39 | None
40 | }
41 |
42 | pub fn extract(content: Value, _domain: &str) -> Result> {
43 | if let Some(items) = content["items"].as_array() {
44 | let filter = |item: &Value| Some(item["hostname"].as_str()?.to_string());
45 |
46 | return Ok(items.iter().filter_map(filter).collect());
47 | }
48 |
49 | Err(JSONExtract.into())
50 | }
51 | }
52 | ```
53 |
--------------------------------------------------------------------------------
/src/types/result/statistics.rs:
--------------------------------------------------------------------------------
1 | use std::collections::HashMap;
2 |
3 | use chrono::{serde::ts_seconds, DateTime, TimeDelta, Utc};
4 | use serde::Serialize;
5 |
6 | use super::status::{SkipReason::SkippedByUser, SubscanModuleStatus};
7 | use crate::utilities::serializers::td_to_seconds;
8 |
9 | /// Subscan result statistics data type
10 | pub type SubscanResultStatistics = HashMap;
11 |
12 | /// Stores single [`SubscanModule`](crate::types::core::SubscanModule) statistics
13 | #[derive(Clone, Debug, Serialize)]
14 | pub struct SubscanModuleStatistic {
15 | pub status: SubscanModuleStatus,
16 | pub count: usize,
17 | #[serde(with = "ts_seconds")]
18 | pub started_at: DateTime,
19 | #[serde(with = "ts_seconds")]
20 | pub finished_at: DateTime,
21 | #[serde(serialize_with = "td_to_seconds")]
22 | pub elapsed: TimeDelta,
23 | }
24 |
25 | impl Default for SubscanModuleStatistic {
26 | fn default() -> Self {
27 | Self {
28 | status: SubscanModuleStatus::Started,
29 | count: 0,
30 | started_at: Utc::now(),
31 | finished_at: Utc::now(),
32 | elapsed: TimeDelta::zero(),
33 | }
34 | }
35 | }
36 |
37 | impl SubscanModuleStatistic {
38 | /// Create skipped module statistics
39 | ///
40 | /// # Examples
41 | ///
42 | /// ```
43 | /// use subscan::types::result::{
44 | /// statistics::SubscanModuleStatistic,
45 | /// status::SkipReason::SkippedByUser,
46 | /// };
47 | ///
48 | /// let skipped = SubscanModuleStatistic::skipped();
49 | ///
50 | /// assert_eq!(skipped.status, SkippedByUser.into());
51 | /// assert_eq!(skipped.count, 0);
52 | /// assert_eq!(skipped.elapsed.num_seconds(), 0);
53 | /// ```
54 | pub fn skipped() -> Self {
55 | Self {
56 | status: SkippedByUser.into(),
57 | count: 0,
58 | started_at: Utc::now(),
59 | finished_at: Utc::now(),
60 | elapsed: TimeDelta::zero(),
61 | }
62 | }
63 |
64 | pub async fn finish_with_status(&mut self, status: SubscanModuleStatus) {
65 | self.finished_at = Utc::now();
66 | self.status = status;
67 | self.elapsed = self.finished_at - self.started_at;
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/tests/components/modules/integrations/censys_test.rs:
--------------------------------------------------------------------------------
1 | use std::env;
2 |
3 | use reqwest::Url;
4 | use serde_json::{json, Value};
5 | use subscan::{
6 | enums::content::Content,
7 | error::ModuleErrorKind::JSONExtract,
8 | interfaces::module::SubscanModuleInterface,
9 | modules::integrations::censys::{Censys, CENSYS_URL},
10 | };
11 |
12 | use crate::common::{
13 | constants::{TEST_BAR_SUBDOMAIN, TEST_DOMAIN, TEST_URL},
14 | mock::funcs,
15 | utils,
16 | };
17 |
18 | #[tokio::test]
19 | #[stubr::mock("module/integrations/censys.json")]
20 | async fn run_test() {
21 | let mut censys = Censys::dispatcher();
22 | let env_name = censys.envs().await.apikey.name;
23 |
24 | env::set_var(&env_name, "censys-api-key");
25 | funcs::wrap_module_url(&mut censys, &stubr.path("/censys"));
26 |
27 | let (results, status) = utils::run_module(censys, TEST_DOMAIN).await;
28 |
29 | assert_eq!(results, [TEST_BAR_SUBDOMAIN.into()].into());
30 | assert_eq!(status, JSONExtract.into());
31 |
32 | env::remove_var(env_name);
33 | }
34 |
35 | #[tokio::test]
36 | async fn get_query_url_test() {
37 | let url = Censys::get_query_url(TEST_DOMAIN);
38 | let expected = format!("{CENSYS_URL}?q={TEST_DOMAIN}");
39 |
40 | assert_eq!(url, expected);
41 | }
42 |
43 | #[tokio::test]
44 | async fn get_next_url_test() {
45 | let url = Url::parse(TEST_URL).unwrap();
46 | let json = json!({"result": {"links": {"next": "foo"}}});
47 | let expected = Url::parse(&format!("{TEST_URL}/?cursor=foo")).unwrap();
48 |
49 | let mut next = Censys::get_next_url(url.clone(), Content::Empty);
50 |
51 | assert!(next.is_none());
52 |
53 | next = Censys::get_next_url(url, json.into());
54 |
55 | assert_eq!(next.unwrap(), expected);
56 | }
57 |
58 | #[tokio::test]
59 | async fn extract_test() {
60 | let stub = "module/integrations/censys.json";
61 | let json = utils::read_stub(stub)["response"]["jsonBody"].clone();
62 |
63 | let extracted = Censys::extract(json, TEST_DOMAIN);
64 | let not_extracted = Censys::extract(Value::Null, TEST_DOMAIN);
65 |
66 | assert!(extracted.is_ok());
67 | assert!(not_extracted.is_err());
68 |
69 | assert_eq!(extracted.unwrap(), [TEST_BAR_SUBDOMAIN.into()].into());
70 | assert_eq!(not_extracted.err().unwrap(), JSONExtract.into());
71 | }
72 |
--------------------------------------------------------------------------------
/src/types/config/pool.rs:
--------------------------------------------------------------------------------
1 | use std::{
2 | fs::{File, OpenOptions},
3 | path::PathBuf,
4 | sync::RwLock,
5 | };
6 |
7 | use super::subscan::SubscanConfig;
8 | use crate::enums::cache::CacheFilter;
9 |
10 | #[derive(Clone, Default)]
11 | pub struct PoolConfig {
12 | pub concurrency: u64,
13 | pub filter: CacheFilter,
14 | pub print: bool,
15 | pub stream: Option,
16 | }
17 |
18 | impl From for PoolConfig {
19 | /// Create [`PoolConfig`] from [`SubscanConfig`]
20 | ///
21 | /// # Examples
22 | ///
23 | /// ```
24 | /// use subscan::types::config::{pool::PoolConfig, subscan::SubscanConfig};
25 | ///
26 | /// let sconfig = SubscanConfig::default();
27 | /// let config = PoolConfig::from(sconfig.clone());
28 | ///
29 | /// assert_eq!(sconfig.concurrency, config.concurrency);
30 | /// assert_eq!(sconfig.filter, config.filter);
31 | /// assert_eq!(sconfig.print, config.print);
32 | /// assert_eq!(sconfig.stream, config.stream);
33 | /// ```
34 | fn from(config: SubscanConfig) -> Self {
35 | Self {
36 | concurrency: config.concurrency,
37 | filter: config.filter,
38 | print: config.print,
39 | stream: config.stream,
40 | }
41 | }
42 | }
43 |
44 | impl PoolConfig {
45 | /// Get stream file descriptor
46 | ///
47 | /// # Examples
48 | ///
49 | /// ```
50 | /// use subscan::types::config::{pool::PoolConfig, subscan::SubscanConfig};
51 | ///
52 | /// #[tokio::main]
53 | /// async fn main() {
54 | /// let sconfig = SubscanConfig::default();
55 | /// let config = PoolConfig::from(sconfig.clone());
56 | ///
57 | /// assert!(config.get_stream_file().await.is_none());
58 | /// }
59 | /// ```
60 | pub async fn get_stream_file(&self) -> Option> {
61 | self.stream.as_ref().map(|path| {
62 | RwLock::new(
63 | OpenOptions::new()
64 | .create(true)
65 | .append(true)
66 | .read(true)
67 | .truncate(false)
68 | .open(path)
69 | .unwrap_or_else(|_| panic!("Cannot create {} file!", path.to_str().unwrap())),
70 | )
71 | })
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/book/src/development/components/extractors.md:
--------------------------------------------------------------------------------
1 | # Extractors
2 |
3 | Extractor components are responsible for parsing subdomain addresses from any `Content` object
4 |
5 | The extractor components already implemented in `Subscan` are as follows
6 |
7 | - [HTMLExtractor](https://docs.rs/subscan/latest/subscan/extractors/html/struct.HTMLExtractor.html)
8 |
9 | Extracts subdomain addresses from inner text by given `XPath` or `CSS` selector
10 |
11 | - [JSONExtractor](https://docs.rs/subscan/latest/subscan/extractors/json/struct.JSONExtractor.html)
12 |
13 | Extracts subdomain addresses from `JSON` content. `JSON` parsing function must be given for this extractor
14 |
15 | - [RegexExtractor](https://docs.rs/subscan/latest/subscan/extractors/regex/struct.RegexExtractor.html)
16 |
17 | Regex extractor component generates subdomain pattern by given domain address and extracts subdomains via this pattern
18 |
19 | ## Create Your Custom Extractor
20 |
21 | Each extractor component should be implemented following the interface below. For a better understanding, you can explore the [docs.rs](https://docs.rs/subscan/latest/subscan/interfaces/extractor/index.html) page and review the crates listed below
22 |
23 | - [`async_trait`](https://github.com/dtolnay/async-trait)
24 | - [`enum_dispatch`](https://gitlab.com/antonok/enum_dispatch)
25 |
26 | ```rust,ignore
27 | #[async_trait]
28 | #[enum_dispatch]
29 | pub trait SubdomainExtractorInterface: Send + Sync {
30 | // Generic extract method, it should extract subdomain addresses
31 | // from given Content
32 | async fn extract(&self, content: Content, domain: &str) -> Result>;
33 | }
34 | ```
35 |
36 | Below is a simple example of a custom extractor. For more examples, you can check the [examples/](https://github.com/eredotpkfr/subscan/tree/main/examples) folder on the project's GitHub page. You can also refer to the source code of predefined requester implementations for a better understanding
37 |
38 | ```rust,ignore
39 | pub struct CustomExtractor {}
40 |
41 | #[async_trait]
42 | impl SubdomainExtractorInterface for CustomExtractor {
43 | async fn extract(&self, content: Content, _domain: &str) -> Result> {
44 | let subdomain = content.as_string().replace("-", "");
45 |
46 | Ok([subdomain].into())
47 | }
48 | }
49 | ```
50 |
--------------------------------------------------------------------------------
/tests/components/common/mock/modules.rs:
--------------------------------------------------------------------------------
1 | use std::collections::BTreeSet;
2 |
3 | use reqwest::Url;
4 | use serde_json::Value;
5 | use subscan::{
6 | enums::{auth::AuthenticationMethod, dispatchers::RequesterDispatcher},
7 | error::ModuleErrorKind::JSONExtract,
8 | extractors::{json::JSONExtractor, regex::RegexExtractor},
9 | modules::generics::{engine::GenericSearchEngineModule, integration::GenericIntegrationModule},
10 | requesters::client::HTTPClient,
11 | types::{
12 | core::SubscanModuleCoreComponents, func::GenericIntegrationCoreFuncs,
13 | query::SearchQueryParam,
14 | },
15 | };
16 |
17 | use super::funcs::wrap_url_with_mock_func;
18 | use crate::common::utils::current_thread_hex;
19 |
20 | pub fn generic_search_engine(url: &str) -> GenericSearchEngineModule {
21 | let requester = RequesterDispatcher::HTTPClient(HTTPClient::default());
22 | let extractor = RegexExtractor::default();
23 | let url = Url::parse(url);
24 |
25 | GenericSearchEngineModule {
26 | name: current_thread_hex(),
27 | url: url.unwrap(),
28 | param: SearchQueryParam::from("q"),
29 | components: SubscanModuleCoreComponents {
30 | requester: requester.into(),
31 | extractor: extractor.into(),
32 | },
33 | }
34 | }
35 |
36 | pub fn generic_integration(url: &str, auth: AuthenticationMethod) -> GenericIntegrationModule {
37 | let parse = |json: Value, _domain: &str| {
38 | if let Some(subdomains) = json["subdomains"].as_array() {
39 | let filter = |item: &Value| Some(item.as_str()?.to_string());
40 |
41 | return Ok(BTreeSet::from_iter(subdomains.iter().filter_map(filter)));
42 | }
43 |
44 | Err(JSONExtract.into())
45 | };
46 |
47 | let requester = RequesterDispatcher::HTTPClient(HTTPClient::default());
48 | let extractor = JSONExtractor::new(Box::new(parse));
49 |
50 | GenericIntegrationModule {
51 | name: current_thread_hex(),
52 | auth,
53 | funcs: GenericIntegrationCoreFuncs {
54 | url: wrap_url_with_mock_func(url),
55 | next: Box::new(|_, _| None),
56 | },
57 | components: SubscanModuleCoreComponents {
58 | requester: requester.into(),
59 | extractor: extractor.into(),
60 | },
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "subscan"
3 | version = "1.3.0"
4 | edition = "2021"
5 | description = "A subdomain enumeration tool leveraging diverse techniques, designed for advanced pentesting operations"
6 | documentation = "https://docs.rs/subscan"
7 | homepage = "https://www.erdoganyoksul.com/subscan"
8 | readme = "README.md"
9 | authors = ["Erdoğan YOKSUL "]
10 | repository = "https://github.com/eredotpkfr/subscan"
11 | license-file = "LICENSE"
12 | keywords = [
13 | "pentesting-tool",
14 | "subdomain-finder",
15 | "bruteforce",
16 | "zonetransfer",
17 | "searchengines",
18 | ]
19 |
20 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
21 |
22 | [dependencies]
23 | async-trait = "0.1.89"
24 | chrono = { version = "0.4.42", features = ["serde"] }
25 | clap = { version = "4.5.53", features = ["derive"] }
26 | clap-verbosity-flag = "3.0.4"
27 | colog = "1.3.0"
28 | colored = "3.0.0"
29 | csv = "1.4.0"
30 | derive_more = { version = "2.0.1", features = ["deref", "display", "from"] }
31 | dotenvy = "0.15.7"
32 | enum_dispatch = "0.3.13"
33 | env_logger = "0.11.8"
34 | flume = "0.11.1"
35 | futures = "0.3.31"
36 | headless_chrome = { version = "1.0.18", default-features = false, features = ["fetch"] }
37 | hickory-client = { version = "0.25.2", default-features = false }
38 | hickory-resolver = { version = "0.25.2", default-features = false, features = ["system-config", "tokio"] }
39 | itertools = "0.14.0"
40 | log = "0.4.28"
41 | prettytable-rs = "0.10.0"
42 | regex = "1.12.2"
43 | reqwest = { version = "0.12.24", features = ["json", "stream"] }
44 | scraper = "0.24.0"
45 | serde = { version = "1.0.228", features = ["derive"] }
46 | serde_json = "1.0.145"
47 | tokio = { version = "1.48.0", features = ["macros", "rt-multi-thread", "tokio-macros"] }
48 | tokio-util = "0.7.17"
49 | url = "2.5.4"
50 |
51 | [dev-dependencies]
52 | automod = "1.0.15"
53 | hickory-server = "0.25.2"
54 | md5 = "0.8.0"
55 | stubr = "0.6.2"
56 | tempfile = "3.23.0"
57 |
58 | [profile.release]
59 | lto = true
60 | strip = true
61 | opt-level = "z"
62 | codegen-units = 1
63 | panic = "abort"
64 |
65 | # The profile that 'dist' will build with
66 | [profile.dist]
67 | inherits = "release"
68 |
69 | [profile.coverage-ci]
70 | inherits = "release"
71 |
72 | [package.metadata.cargo-machete]
73 | ignored = ["prettytable-rs"]
74 |
--------------------------------------------------------------------------------
/tests/components/modules/integrations/certspotter_test.rs:
--------------------------------------------------------------------------------
1 | use std::env;
2 |
3 | use reqwest::Url;
4 | use serde_json::Value;
5 | use subscan::{
6 | enums::content::Content,
7 | error::ModuleErrorKind::JSONExtract,
8 | interfaces::module::SubscanModuleInterface,
9 | modules::integrations::certspotter::{CertSpotter, CERTSPOTTER_URL},
10 | types::result::status::SubscanModuleStatus,
11 | };
12 |
13 | use crate::common::{
14 | constants::{TEST_BAR_SUBDOMAIN, TEST_DOMAIN, TEST_URL},
15 | mock::funcs,
16 | utils,
17 | };
18 |
19 | #[tokio::test]
20 | #[stubr::mock("module/integrations/certspotter.json")]
21 | async fn run_test() {
22 | let mut certspotter = CertSpotter::dispatcher();
23 | let env_name = certspotter.envs().await.apikey.name;
24 |
25 | env::set_var(&env_name, "certspotter-api-key");
26 | funcs::wrap_module_url(&mut certspotter, &stubr.path("/certspotter"));
27 |
28 | let (results, status) = utils::run_module(certspotter, TEST_DOMAIN).await;
29 |
30 | assert_eq!(results, [TEST_BAR_SUBDOMAIN.into()].into());
31 | assert_eq!(status, SubscanModuleStatus::Finished);
32 |
33 | env::remove_var(env_name);
34 | }
35 |
36 | #[tokio::test]
37 | async fn get_query_url_test() {
38 | let url = CertSpotter::get_query_url(TEST_DOMAIN);
39 |
40 | let params = &[
41 | ("domain", TEST_DOMAIN),
42 | ("include_subdomains", "true"),
43 | ("expand", "dns_names"),
44 | ];
45 |
46 | let expected = Url::parse_with_params(CERTSPOTTER_URL, params);
47 |
48 | assert_eq!(url, expected.unwrap().to_string());
49 | }
50 |
51 | #[tokio::test]
52 | async fn get_next_url_test() {
53 | let url = TEST_URL.parse().unwrap();
54 | let next = CertSpotter::get_next_url(url, Content::Empty);
55 |
56 | assert!(next.is_none());
57 | }
58 |
59 | #[tokio::test]
60 | async fn extract_test() {
61 | let stub = "module/integrations/certspotter.json";
62 | let json = utils::read_stub(stub)["response"]["jsonBody"].clone();
63 |
64 | let extracted = CertSpotter::extract(json, TEST_DOMAIN);
65 | let not_extracted = CertSpotter::extract(Value::Null, TEST_DOMAIN);
66 |
67 | assert!(extracted.is_ok());
68 | assert!(not_extracted.is_err());
69 |
70 | assert_eq!(extracted.unwrap(), [TEST_BAR_SUBDOMAIN.into()].into());
71 | assert_eq!(not_extracted.err().unwrap(), JSONExtract.into());
72 | }
73 |
--------------------------------------------------------------------------------
/tests/components/modules/integrations/binaryedge_test.rs:
--------------------------------------------------------------------------------
1 | use std::env;
2 |
3 | use reqwest::Url;
4 | use serde_json::Value;
5 | use subscan::{
6 | enums::content::Content,
7 | error::ModuleErrorKind::JSONExtract,
8 | interfaces::module::SubscanModuleInterface,
9 | modules::integrations::binaryedge::{BinaryEdge, BINARYEDGE_URL},
10 | };
11 |
12 | use crate::common::{
13 | constants::{TEST_BAR_SUBDOMAIN, TEST_DOMAIN, TEST_URL},
14 | mock::funcs,
15 | utils,
16 | };
17 |
18 | #[tokio::test]
19 | #[stubr::mock("module/integrations/binaryedge.json")]
20 | async fn run_test() {
21 | let mut binaryedge = BinaryEdge::dispatcher();
22 | let env_name = binaryedge.envs().await.apikey.name;
23 |
24 | env::set_var(&env_name, "binaryedge-api-key");
25 | funcs::wrap_module_url(&mut binaryedge, &stubr.path("/binaryedge"));
26 |
27 | let (results, status) = utils::run_module(binaryedge, TEST_DOMAIN).await;
28 |
29 | assert_eq!(results, [TEST_BAR_SUBDOMAIN.into()].into());
30 | assert_eq!(status, JSONExtract.into());
31 |
32 | env::remove_var(env_name);
33 | }
34 |
35 | #[tokio::test]
36 | async fn get_query_url_test() {
37 | let url = BinaryEdge::get_query_url(TEST_DOMAIN);
38 | let expected = format!("{BINARYEDGE_URL}/{TEST_DOMAIN}");
39 |
40 | assert_eq!(url, expected);
41 | }
42 |
43 | #[tokio::test]
44 | async fn get_next_url_test() {
45 | let url = Url::parse(TEST_URL).unwrap();
46 |
47 | let mut next = BinaryEdge::get_next_url(url.clone(), Content::Empty).unwrap();
48 | let mut expected = Url::parse(&format!("{TEST_URL}/?page=2")).unwrap();
49 |
50 | assert_eq!(next, expected);
51 |
52 | next = BinaryEdge::get_next_url(next, Content::Empty).unwrap();
53 | expected = Url::parse(&format!("{TEST_URL}/?page=3")).unwrap();
54 |
55 | assert_eq!(next, expected);
56 | }
57 |
58 | #[tokio::test]
59 | async fn extract_test() {
60 | let stub = "module/integrations/binaryedge.json";
61 | let json = utils::read_stub(stub)["response"]["jsonBody"].clone();
62 |
63 | let extracted = BinaryEdge::extract(json, TEST_DOMAIN);
64 | let not_extracted = BinaryEdge::extract(Value::Null, TEST_DOMAIN);
65 |
66 | assert!(extracted.is_ok());
67 | assert!(not_extracted.is_err());
68 |
69 | assert_eq!(extracted.unwrap(), [TEST_BAR_SUBDOMAIN.into()].into());
70 | assert_eq!(not_extracted.err().unwrap(), JSONExtract.into());
71 | }
72 |
--------------------------------------------------------------------------------
/examples/custom_module.rs:
--------------------------------------------------------------------------------
1 | use std::collections::BTreeSet;
2 |
3 | use async_trait::async_trait;
4 | use flume::Sender;
5 | use subscan::{
6 | enums::{
7 | dispatchers::{RequesterDispatcher, SubdomainExtractorDispatcher},
8 | result::{OptionalSubscanModuleResult, SubscanModuleResult},
9 | },
10 | extractors::regex::RegexExtractor,
11 | interfaces::module::SubscanModuleInterface,
12 | requesters::client::HTTPClient,
13 | types::{core::Subdomain, result::item::SubscanModuleResultItem},
14 | };
15 | use tokio::sync::Mutex;
16 |
17 | pub struct CustomModule {
18 | pub requester: Mutex,
19 | pub extractor: SubdomainExtractorDispatcher,
20 | }
21 |
22 | #[async_trait]
23 | impl SubscanModuleInterface for CustomModule {
24 | async fn name(&self) -> &str {
25 | &"name"
26 | }
27 |
28 | async fn requester(&self) -> Option<&Mutex> {
29 | Some(&self.requester)
30 | }
31 |
32 | async fn extractor(&self) -> Option<&SubdomainExtractorDispatcher> {
33 | Some(&self.extractor)
34 | }
35 |
36 | async fn run(&mut self, _domain: &str, results: Sender) {
37 | let subdomains = BTreeSet::from_iter([Subdomain::from("bar.foo.com")]);
38 |
39 | for subdomain in &subdomains {
40 | results.send((self.name().await, subdomain).into()).unwrap();
41 | }
42 | }
43 | }
44 |
45 | #[tokio::main]
46 | async fn main() {
47 | let requester: RequesterDispatcher = HTTPClient::default().into();
48 | let extracator: RegexExtractor = RegexExtractor::default();
49 |
50 | let (tx, rx) = flume::unbounded::();
51 |
52 | let mut module = CustomModule {
53 | requester: requester.into(),
54 | extractor: extracator.into(),
55 | };
56 |
57 | assert!(module.requester().await.is_some());
58 | assert!(module.extractor().await.is_some());
59 |
60 | assert_eq!(module.name().await, "name");
61 |
62 | module.run("foo.com", tx).await;
63 |
64 | let result = rx.recv().unwrap();
65 | let item = SubscanModuleResultItem {
66 | module: "name".into(),
67 | subdomain: Subdomain::from("bar.foo.com"),
68 | };
69 | let expected = &SubscanModuleResult::SubscanModuleResultItem(item);
70 |
71 | assert!(result.is_some());
72 | assert_eq!(result.as_ref().unwrap(), expected);
73 | }
74 |
--------------------------------------------------------------------------------