use std::process::ExitCode;
use hackerone_api::{Client, CreateHackerReport, SeverityRating};
fn value(arg: Option<String>, env: &str) -> Option<String> {
arg.filter(|s| !s.is_empty())
.or_else(|| std::env::var(env).ok().filter(|s| !s.is_empty()))
}
fn parse_severity(raw: &str) -> Result<SeverityRating, String> {
match raw.trim().to_ascii_lowercase().as_str() {
"none" => Ok(SeverityRating::None),
"low" => Ok(SeverityRating::Low),
"medium" => Ok(SeverityRating::Medium),
"high" => Ok(SeverityRating::High),
"critical" => Ok(SeverityRating::Critical),
other => Err(format!(
"severity must be none|low|medium|high|critical, got {other:?}"
)),
}
}
fn main() -> ExitCode {
let args: Vec<String> = std::env::args().skip(1).collect();
let mut args = args.into_iter();
let program = value(args.next(), "H1_PROGRAM");
let title = value(args.next(), "H1_TITLE");
let vuln_file = value(args.next(), "H1_VULN_FILE");
let impact = value(args.next(), "H1_IMPACT");
let severity = value(args.next(), "H1_SEVERITY");
let weakness_id = value(args.next(), "H1_WEAKNESS_ID");
let scope_id = value(args.next(), "H1_SCOPE_ID");
let (Some(program), Some(title), Some(vuln_file), Some(impact), Some(severity)) =
(program, title, vuln_file, impact, severity)
else {
eprintln!(
"usage: submit_report <program> <title> <vuln.md> <impact> <severity> \
[weakness_id] [structured_scope_id]\n\
(or set H1_PROGRAM/H1_TITLE/H1_VULN_FILE/H1_IMPACT/H1_SEVERITY[/H1_WEAKNESS_ID/H1_SCOPE_ID])"
);
return ExitCode::from(2);
};
let severity = match parse_severity(&severity) {
Ok(s) => s,
Err(e) => {
eprintln!("error: {e}");
return ExitCode::from(2);
}
};
let vulnerability_information = match std::fs::read_to_string(&vuln_file) {
Ok(text) => text,
Err(e) => {
eprintln!("error: cannot read {vuln_file}: {e}");
return ExitCode::from(2);
}
};
let mut report = CreateHackerReport::new(program.clone(), title.clone())
.vulnerability_information(vulnerability_information)
.impact(impact)
.severity(severity);
if let Some(raw) = weakness_id {
match raw.parse::<u64>() {
Ok(id) => report = report.weakness_id(id),
Err(_) => {
eprintln!("error: weakness_id must be an integer, got {raw:?}");
return ExitCode::from(2);
}
}
}
if let Some(raw) = scope_id {
match raw.parse::<u64>() {
Ok(id) => report = report.structured_scope_id(id),
Err(_) => {
eprintln!("error: structured_scope_id must be an integer, got {raw:?}");
return ExitCode::from(2);
}
}
}
let payload = match report.to_json() {
Ok(value) => value,
Err(e) => {
eprintln!("error: {e}");
return ExitCode::from(2);
}
};
println!(
"▶ POST /v1/hackers/reports (program={program:?}, severity={:?})",
severity.as_str()
);
println!("{}", serde_json::to_string_pretty(&payload).unwrap());
if std::env::var("H1_API_SUBMIT").as_deref() != Ok("1") {
println!("\n(preview only — set H1_API_SUBMIT=1 to actually submit)");
return ExitCode::SUCCESS;
}
let identifier = std::env::var("HACKERONE_API_IDENTIFIER")
.expect("set HACKERONE_API_IDENTIFIER (token identifier)");
let token =
std::env::var("HACKERONE_API_TOKEN").expect("set HACKERONE_API_TOKEN (token value)");
let client = Client::new(identifier, token);
println!("\n=== SUBMITTING ===");
match client.create_report(&report) {
Ok(created) => {
println!("submitted");
println!(
" title: {}",
created.title.as_deref().unwrap_or("(untitled)")
);
println!(" state: {}", created.state.as_deref().unwrap_or("?"));
ExitCode::SUCCESS
}
Err(e) => {
eprintln!("submission failed: {e}");
if e.is_client_error() {
for api_error in e.api_errors() {
eprintln!(
" - {}: {}",
api_error.title.as_deref().unwrap_or("error"),
api_error.detail.as_deref().unwrap_or("")
);
}
}
ExitCode::FAILURE
}
}
}