use crate::CheckArgs;
use crate::feedback::{FalsePositiveReport, ReportSubmitter, SubmitTarget};
use colored::Colorize;
use std::io::{self, BufRead, Write};
use std::process::ExitCode;
const MAX_INPUT_LENGTH: usize = 10_000;
fn read_line_limited(stdin: &io::Stdin, max_len: usize) -> io::Result<String> {
let mut line = String::new();
stdin.lock().read_line(&mut line)?;
if line.len() > max_len {
line.truncate(max_len);
}
Ok(line)
}
pub fn handle_report_fp(args: &CheckArgs) -> ExitCode {
println!("{}", "False Positive Report".bold());
println!("{}", "═".repeat(40));
println!();
if args.no_telemetry {
eprintln!(
"{}",
"Telemetry is disabled. Report will not be submitted.".yellow()
);
return ExitCode::from(2);
}
let stdin = io::stdin();
let mut stdout = io::stdout();
print!("Rule ID (e.g., SL-001): ");
if stdout.flush().is_err() {
eprintln!("{}", "Error: Failed to write to stdout".red());
return ExitCode::from(2);
}
let rule_id = match read_line_limited(&stdin, 100) {
Ok(line) if !line.trim().is_empty() => line.trim().to_uppercase(),
_ => {
eprintln!("{}", "Error: Rule ID is required".red());
return ExitCode::from(2);
}
};
if !is_valid_rule_id(&rule_id) {
eprintln!(
"{}",
"Error: Invalid rule ID format. Expected format: XX-NNN (e.g., SL-001)".red()
);
return ExitCode::from(2);
}
print!("File extension (optional, e.g., js, py): ");
let _ = stdout.flush();
let extension = read_line_limited(&stdin, 50)
.ok()
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.map(|s| s.trim_start_matches('.').to_string());
println!("Description (why is this a false positive?):");
print!("> ");
let _ = stdout.flush();
let description = read_line_limited(&stdin, MAX_INPUT_LENGTH)
.ok()
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty());
let mut report = FalsePositiveReport::new(&rule_id);
if let Some(ext) = extension {
report = report.with_extension(ext);
}
if let Some(desc) = description {
report = report.with_description(desc);
}
if let Ok(username) = std::env::var("USER").or_else(|_| std::env::var("USERNAME")) {
report = report.with_anonymous_id(username.as_bytes());
}
println!();
println!("{}", "Report Preview:".bold());
println!("{}", "-".repeat(40));
println!("{}", report.to_github_issue_body());
println!("{}", "-".repeat(40));
print!("Submit this report? [y/N]: ");
let _ = stdout.flush();
let confirm = read_line_limited(&stdin, 10)
.map(|s| s.trim().to_lowercase())
.unwrap_or_default();
if confirm != "y" && confirm != "yes" {
println!("{}", "Report cancelled.".yellow());
return ExitCode::SUCCESS;
}
let target = if args.report_fp_dry_run {
SubmitTarget::DryRun
} else if let Some(endpoint) = &args.report_fp_endpoint {
SubmitTarget::Endpoint(endpoint.clone())
} else {
SubmitTarget::default()
};
let submitter = ReportSubmitter::new().with_target(target);
println!();
println!("{}", "Submitting report...".cyan());
match submitter.submit(&report) {
Ok(result) => {
if result.success {
if let Some(url) = result.issue_url {
println!("{} Report submitted successfully!", "✓".green());
println!("Issue URL: {}", url.cyan());
} else {
println!("{} Report processed.", "✓".green());
}
ExitCode::SUCCESS
} else {
if let Some(error) = result.error {
eprintln!("{} Submission failed: {}", "✗".red(), error);
} else {
eprintln!("{} Submission failed.", "✗".red());
}
ExitCode::from(1)
}
}
Err(e) => {
eprintln!("{} {}", "Error:".red(), e);
ExitCode::from(2)
}
}
}
fn is_valid_rule_id(id: &str) -> bool {
let parts: Vec<&str> = id.split('-').collect();
if parts.len() != 2 {
return false;
}
let prefix = parts[0];
if prefix.len() < 2 || prefix.len() > 4 || !prefix.chars().all(|c| c.is_ascii_uppercase()) {
return false;
}
let suffix = parts[1];
if suffix.is_empty() || suffix.len() > 4 || !suffix.chars().all(|c| c.is_ascii_digit()) {
return false;
}
true
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_valid_rule_ids() {
assert!(is_valid_rule_id("SL-001"));
assert!(is_valid_rule_id("EX-002"));
assert!(is_valid_rule_id("PI-003"));
assert!(is_valid_rule_id("OP-1"));
assert!(is_valid_rule_id("MALW-0001"));
assert!(is_valid_rule_id("CVE-1234"));
}
#[test]
fn test_invalid_rule_ids() {
assert!(!is_valid_rule_id("")); assert!(!is_valid_rule_id("SL001")); assert!(!is_valid_rule_id("sl-001")); assert!(!is_valid_rule_id("S-001")); assert!(!is_valid_rule_id("SL-")); assert!(!is_valid_rule_id("-001")); assert!(!is_valid_rule_id("ABCDE-001")); assert!(!is_valid_rule_id("SL-12345")); assert!(!is_valid_rule_id("SL-ABC")); }
#[test]
fn test_max_input_length_constant() {
assert_eq!(MAX_INPUT_LENGTH, 10_000);
}
}