Skip to content

Commit 2a03ab3

Browse files
Copilot0xrinegade
andcommitted
Implement requested changes: default to osvm.ai, AI enabled by default, GitHub repo parsing
Co-authored-by: 0xrinegade <[email protected]>
1 parent a19be56 commit 2a03ab3

File tree

5 files changed

+163
-52
lines changed

5 files changed

+163
-52
lines changed

src/clparse.rs

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -802,6 +802,12 @@ pub fn parse_command_line() -> clap::ArgMatches {
802802
.subcommand(
803803
Command::new("audit")
804804
.about("Generate comprehensive security audit report")
805+
.arg(
806+
Arg::new("repository")
807+
.help("Repository to audit (format: owner/repo or owner/repo#branch)")
808+
.value_name("REPOSITORY")
809+
.index(1)
810+
)
805811
.arg(
806812
Arg::new("output")
807813
.long("output")
@@ -832,10 +838,16 @@ pub fn parse_command_line() -> clap::ArgMatches {
832838
.help("Generate test audit report with sample data")
833839
)
834840
.arg(
835-
Arg::new("ai-analysis")
836-
.long("ai-analysis")
841+
Arg::new("noai")
842+
.long("noai")
837843
.action(ArgAction::SetTrue)
838-
.help("Enable AI-powered security analysis using OpenAI (requires OPENAI_API_KEY)")
844+
.help("Disable AI-powered security analysis")
845+
)
846+
.arg(
847+
Arg::new("api-url")
848+
.long("api-url")
849+
.value_name("URL")
850+
.help("Custom API URL for AI analysis (default: https://osvm.ai/api/getAnswer)")
839851
)
840852
.arg(
841853
Arg::new("gh")

src/main.rs

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -205,9 +205,31 @@ async fn handle_audit_command(
205205
let format = matches.get_one::<String>("format").unwrap().to_string();
206206
let verbose = matches.get_count("verbose");
207207
let test_mode = matches.get_flag("test");
208-
let ai_analysis = matches.get_flag("ai-analysis");
209-
let gh_repo = matches.get_one::<String>("gh").map(|s| s.to_string());
208+
209+
// AI analysis is enabled by default, disabled only if --noai is provided
210+
let ai_analysis = !matches.get_flag("noai");
211+
212+
// Handle repository parsing - check positional argument first, then --gh flag
213+
let gh_repo = if let Some(repo) = matches.get_one::<String>("repository") {
214+
// Parse positional argument as GitHub repo
215+
if repo.contains('/') {
216+
// Looks like owner/repo format
217+
if repo.contains('#') {
218+
Some(repo.to_string())
219+
} else {
220+
// No branch specified, try main first, then default branch
221+
Some(format!("{}#main", repo))
222+
}
223+
} else {
224+
// Not a repo format, treat as regular path
225+
None
226+
}
227+
} else {
228+
matches.get_one::<String>("gh").map(|s| s.to_string())
229+
};
230+
210231
let template_path = matches.get_one::<String>("template").map(|s| s.to_string());
232+
let api_url = matches.get_one::<String>("api-url").map(|s| s.to_string());
211233

212234
let request = AuditRequest {
213235
output_dir,
@@ -218,22 +240,21 @@ async fn handle_audit_command(
218240
gh_repo,
219241
template_path,
220242
no_commit,
243+
api_url,
221244
};
222245

223-
// Create the audit service with or without AI
246+
// Create the audit service with custom API URL if provided
224247
let service = if ai_analysis {
225-
// Check if OpenAI API key is available, otherwise use internal AI
226-
match std::env::var("OPENAI_API_KEY") {
227-
Ok(api_key) if !api_key.trim().is_empty() => {
228-
println!("🤖 Using OpenAI API with provided key");
229-
AuditService::with_ai(api_key)
230-
}
231-
_ => {
232-
println!("🤖 Using internal OSVM AI service");
233-
AuditService::with_internal_ai()
234-
}
248+
if let Some(api_url) = &request.api_url {
249+
println!("🤖 Using custom AI API: {}", api_url);
250+
AuditService::with_custom_ai(api_url.clone())
251+
} else {
252+
// Use default (osvm.ai unless explicitly configured for OpenAI)
253+
println!("🤖 Using default OSVM AI service");
254+
AuditService::with_internal_ai()
235255
}
236256
} else {
257+
println!("🤖 AI analysis disabled");
237258
AuditService::new()
238259
};
239260

src/services/ai_service.rs

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,19 +49,45 @@ pub struct AiService {
4949

5050
impl AiService {
5151
pub fn new() -> Self {
52-
let openai_url = env::var("OPENAI_URL").ok();
53-
let openai_key = env::var("OPENAI_KEY").ok();
54-
55-
let (api_url, use_openai) = if let (Some(url), Some(_)) = (&openai_url, &openai_key) {
56-
(url.clone(), true)
52+
Self::with_api_url(None)
53+
}
54+
55+
pub fn with_api_url(custom_api_url: Option<String>) -> Self {
56+
let (api_url, use_openai) = match custom_api_url {
57+
Some(url) => {
58+
// Check if it's an OpenAI URL and we have an API key
59+
if url.contains("openai.com") || url.contains("api.openai.com") {
60+
if let Some(key) = env::var("OPENAI_KEY").ok().filter(|k| !k.trim().is_empty()) {
61+
(url, true)
62+
} else {
63+
eprintln!("⚠️ OpenAI URL provided but no OPENAI_KEY found, falling back to OSVM AI");
64+
("https://osvm.ai/api/getAnswer".to_string(), false)
65+
}
66+
} else {
67+
// Custom URL, treat as external API
68+
(url, false)
69+
}
70+
}
71+
None => {
72+
// Default behavior: use osvm.ai unless explicitly configured for OpenAI
73+
if let (Some(url), Some(_)) = (env::var("OPENAI_URL").ok(), env::var("OPENAI_KEY").ok()) {
74+
(url, true)
75+
} else {
76+
("https://osvm.ai/api/getAnswer".to_string(), false)
77+
}
78+
}
79+
};
80+
81+
let api_key = if use_openai {
82+
env::var("OPENAI_KEY").ok()
5783
} else {
58-
("https://osvm.ai/api/getAnswer".to_string(), false)
84+
None
5985
};
6086

6187
Self {
6288
client: reqwest::Client::new(),
6389
api_url,
64-
api_key: openai_key,
90+
api_key,
6591
use_openai,
6692
}
6793
}

src/services/audit_service.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ pub struct AuditRequest {
3636
pub gh_repo: Option<String>,
3737
pub template_path: Option<String>,
3838
pub no_commit: bool,
39+
pub api_url: Option<String>,
3940
}
4041

4142
pub struct AuditResult {
@@ -69,6 +70,12 @@ impl AuditService {
6970
}
7071
}
7172

73+
pub fn with_custom_ai(api_url: String) -> Self {
74+
Self {
75+
coordinator: AuditCoordinator::with_custom_ai(api_url),
76+
}
77+
}
78+
7279
pub fn with_optional_ai(api_key: Option<String>) -> Self {
7380
Self {
7481
coordinator: AuditCoordinator::with_optional_ai(api_key),

src/utils/audit.rs

Lines changed: 74 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -952,6 +952,21 @@ impl AuditCoordinator {
952952
}
953953
}
954954

955+
/// Create a new audit coordinator with custom AI API
956+
pub fn with_custom_ai(api_url: String) -> Self {
957+
Self {
958+
ai_client: None,
959+
internal_ai_service: Some(AiService::with_api_url(Some(api_url))),
960+
modular_coordinator: ModularAuditCoordinator::new(),
961+
template_generator: TemplateReportGenerator::new().unwrap_or_else(|e| {
962+
log::warn!("Failed to initialize template generator: {}", e);
963+
panic!("Template generator is required for audit functionality");
964+
}),
965+
ai_disabled: false,
966+
ai_circuit_breaker: AICircuitBreaker::new(3, Duration::from_secs(300)), // 3 failures, 5 min recovery
967+
}
968+
}
969+
955970
/// Create a new audit coordinator with optional AI capabilities
956971
/// Now defaults to internal AI service, with fallback to OpenAI if api_key is provided
957972
pub fn with_optional_ai(api_key: Option<String>) -> Self {
@@ -7176,58 +7191,88 @@ This security audit provides a comprehensive assessment of the OSVM CLI applicat
71767191
Ok(report)
71777192
}
71787193

7179-
/// Parse repository specification (owner/repo#branch)
7194+
/// Parse repository specification (owner/repo#branch or owner/repo)
71807195
fn parse_repo_spec(&self, repo_spec: &str) -> Result<(String, String)> {
71817196
let parts: Vec<&str> = repo_spec.split('#').collect();
7182-
if parts.len() != 2 {
7183-
anyhow::bail!("Invalid repository specification. Expected format: owner/repo#branch");
7197+
7198+
if parts.len() > 2 {
7199+
anyhow::bail!("Invalid repository specification. Expected format: owner/repo or owner/repo#branch");
71847200
}
71857201

71867202
let repo_path = parts[0];
7187-
let branch = parts[1];
7203+
let branch = if parts.len() == 2 {
7204+
parts[1].to_string()
7205+
} else {
7206+
// No branch specified, default to "main"
7207+
"main".to_string()
7208+
};
71887209

71897210
if !repo_path.contains('/') {
71907211
anyhow::bail!("Invalid repository path. Expected format: owner/repo");
71917212
}
71927213

71937214
let repo_url = format!("https://github.com/{}.git", repo_path);
7194-
Ok((repo_url, branch.to_string()))
7215+
Ok((repo_url, branch))
71957216
}
71967217

7197-
/// Clone repository to temporary directory
7218+
/// Clone repository to temporary directory with branch fallback
71987219
fn clone_repository(&self, repo_url: &str, branch: &str) -> Result<std::path::PathBuf> {
71997220
let temp_dir =
72007221
std::env::temp_dir().join(format!("osvm-audit-{}", chrono::Utc::now().timestamp()));
72017222

72027223
println!("📥 Cloning repository to: {}", temp_dir.display());
72037224

7204-
// Use timeout for git operations to prevent hanging
7205-
let output = self.execute_git_with_timeout(
7206-
&[
7207-
"clone",
7208-
"--branch",
7209-
branch,
7210-
"--single-branch",
7211-
repo_url,
7212-
temp_dir.to_str().unwrap(),
7213-
],
7214-
Some(&std::env::temp_dir()),
7215-
Duration::from_secs(300), // 5 minute timeout
7216-
)?;
7225+
// Try to clone with the specified branch first
7226+
let branches_to_try = if branch == "main" {
7227+
// If user didn't specify a branch and we defaulted to "main", try fallbacks
7228+
vec!["main", "master", "develop"]
7229+
} else {
7230+
// User specified a branch, only try that one
7231+
vec![branch]
7232+
};
72177233

7218-
if !output.status.success() {
7219-
let error_msg = String::from_utf8_lossy(&output.stderr);
7220-
anyhow::bail!("Git clone failed: {}", error_msg);
7221-
}
7234+
let mut last_error = String::new();
72227235

7223-
// Verify the repository was cloned successfully
7224-
if !temp_dir.exists() {
7225-
anyhow::bail!(
7226-
"Repository clone directory does not exist: {}",
7227-
temp_dir.display()
7228-
);
7236+
for branch_name in branches_to_try {
7237+
println!("🌿 Trying branch: {}", branch_name);
7238+
7239+
// Use timeout for git operations to prevent hanging
7240+
let output = self.execute_git_with_timeout(
7241+
&[
7242+
"clone",
7243+
"--branch",
7244+
branch_name,
7245+
"--single-branch",
7246+
repo_url,
7247+
temp_dir.to_str().unwrap(),
7248+
],
7249+
Some(&std::env::temp_dir()),
7250+
Duration::from_secs(180), // 3 minute timeout per attempt
7251+
)?;
7252+
7253+
if output.status.success() {
7254+
// Verify the repository was cloned successfully
7255+
if temp_dir.exists() {
7256+
println!("✅ Successfully cloned with branch: {}", branch_name);
7257+
return Ok(temp_dir);
7258+
} else {
7259+
last_error = format!("Repository clone directory does not exist: {}", temp_dir.display());
7260+
break;
7261+
}
7262+
} else {
7263+
last_error = String::from_utf8_lossy(&output.stderr).to_string();
7264+
println!("❌ Failed to clone with branch '{}': {}", branch_name, last_error);
7265+
7266+
// Clean up any partial clone attempt
7267+
if temp_dir.exists() {
7268+
let _ = std::fs::remove_dir_all(&temp_dir);
7269+
}
7270+
}
72297271
}
72307272

7273+
anyhow::bail!("Git clone failed for all attempted branches. Last error: {}", last_error)
7274+
}
7275+
72317276
Ok(temp_dir)
72327277
}
72337278

0 commit comments

Comments
 (0)