use std::net::IpAddr;
use std::path::Path;
use std::time::Duration;
use serde::{Deserialize, Serialize};
use crate::Severity;
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ApiKeys {
pub virustotal: Option<String>,
pub securitytrails: Option<String>,
pub shodan: Option<String>,
pub github: Option<String>,
}
impl ApiKeys {
pub fn resolve(mut self) -> Self {
if let Ok(v) = std::env::var("VT_API_KEY") {
self.virustotal = Some(v);
}
if let Ok(v) = std::env::var("ST_API_KEY") {
self.securitytrails = Some(v);
}
if let Ok(v) = std::env::var("SHODAN_API_KEY") {
self.shodan = Some(v);
}
if let Ok(v) = std::env::var("GITHUB_TOKEN") {
self.github = Some(v);
}
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum PortMode {
#[default]
Default,
Top100,
Top1000,
Full,
Custom(Vec<u16>),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CrawlConfig {
pub max_pages: usize,
pub max_depth: usize,
}
impl Default for CrawlConfig {
fn default() -> Self {
Self {
max_pages: 50,
max_depth: 3,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
pub rate_limit: u32,
pub timeout_secs: u64,
pub concurrency: usize,
pub resolvers: Vec<IpAddr>,
pub user_agent: String,
pub proxy: Option<String>,
pub cookie: Option<String>,
pub modules: ModuleConfig,
pub output: OutputConfig,
pub min_severity: Option<Severity>,
pub port_mode: PortMode,
pub api_keys: ApiKeys,
#[serde(default)]
pub crawl: CrawlConfig,
}
impl Config {
pub fn timeout(&self) -> Duration {
Duration::from_secs(self.timeout_secs)
}
pub fn from_toml(path: &Path) -> Result<Self, String> {
let content = std::fs::read_to_string(path)
.map_err(|e| format!("failed to read config {}: {e}", path.display()))?;
toml::from_str(&content)
.map_err(|e| format!("failed to parse config {}: {e}", path.display()))
}
pub fn load_or_default() -> Result<Self, String> {
let path = Path::new("gossan.toml");
if path.exists() {
Self::from_toml(path)
} else {
Ok(Self::default())
}
}
}
impl Default for Config {
fn default() -> Self {
Self {
rate_limit: 300,
timeout_secs: 10,
concurrency: 200,
resolvers: vec![
IpAddr::V4(std::net::Ipv4Addr::new(1, 1, 1, 1)),
IpAddr::V4(std::net::Ipv4Addr::new(8, 8, 8, 8)),
],
user_agent: concat!(
"gossan/",
env!("CARGO_PKG_VERSION"),
" (+https://github.com/santhsecurity/gossan)"
)
.to_string(),
proxy: None,
cookie: None,
modules: ModuleConfig::default(),
output: OutputConfig::default(),
min_severity: None,
port_mode: PortMode::Default,
api_keys: ApiKeys::default(),
crawl: CrawlConfig::default(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ModuleConfig {
pub subdomain: bool,
pub portscan: bool,
pub techstack: bool,
pub dns: bool,
pub js: bool,
pub hidden: bool,
pub cloud: bool,
pub synscan: bool,
pub headless: bool,
pub crawl: bool,
}
impl ModuleConfig {
pub fn all() -> Self {
Self {
subdomain: true,
portscan: true,
techstack: true,
dns: true,
js: true,
hidden: true,
cloud: true,
synscan: true,
headless: true,
crawl: true,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum OutputFormat {
Json,
Jsonl,
Sarif,
Text,
Markdown,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OutputConfig {
pub format: OutputFormat,
pub path: Option<String>,
}
impl Default for OutputConfig {
fn default() -> Self {
Self {
format: OutputFormat::Text,
path: None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn config_default_timeout_matches_timeout_secs() {
let config = Config::default();
assert_eq!(config.timeout(), Duration::from_secs(config.timeout_secs));
}
#[test]
fn module_config_all_enables_every_module() {
let modules = ModuleConfig::all();
assert!(modules.subdomain);
assert!(modules.portscan);
assert!(modules.techstack);
assert!(modules.dns);
assert!(modules.js);
assert!(modules.hidden);
assert!(modules.cloud);
assert!(modules.synscan);
assert!(modules.headless);
assert!(modules.crawl);
}
#[test]
fn output_config_defaults_to_text_and_no_path() {
let output = OutputConfig::default();
assert!(matches!(output.format, OutputFormat::Text));
assert_eq!(output.path, None);
}
#[test]
fn port_mode_serializes_snake_case_variants() {
assert_eq!(
serde_json::to_value(PortMode::Default).unwrap(),
json!("default")
);
assert_eq!(
serde_json::to_value(PortMode::Top100).unwrap(),
json!("top100")
);
assert_eq!(
serde_json::to_value(PortMode::Top1000).unwrap(),
json!("top1000")
);
assert_eq!(serde_json::to_value(PortMode::Full).unwrap(), json!("full"));
}
}