hackerone-api 0.2.0

Unofficial, dependency-light Rust client for the HackerOne API (v1): submit reports, read your reports, hacktivity, balance, and earnings.
Documentation
//! Submit a report to a program as a hacker (`POST /v1/hackers/reports`).
//!
//! Usage (args, or the matching env vars):
//!
//! ```text
//! submit_report <program> <title> <vuln.md> <impact> <severity> [weakness_id] [structured_scope_id]
//! ```
//!
//! | arg | env | notes |
//! |---|---|---|
//! | program | `H1_PROGRAM` | program handle, e.g. `chia_network` |
//! | title | `H1_TITLE` | report title |
//! | vuln.md | `H1_VULN_FILE` | path to the markdown write-up |
//! | impact | `H1_IMPACT` | security impact |
//! | severity | `H1_SEVERITY` | `none\|low\|medium\|high\|critical` |
//! | weakness_id | `H1_WEAKNESS_ID` | optional CWE/weakness id |
//! | structured_scope_id | `H1_SCOPE_ID` | optional structured-scope id |
//!
//! Auth (required):
//!
//! ```sh
//! export HACKERONE_API_IDENTIFIER=...
//! export HACKERONE_API_TOKEN=...
//! ```
//!
//! **Safety:** real submission is gated behind `H1_API_SUBMIT=1`. Without it
//! the example prints the exact JSON:API payload and exits without touching the
//! network — so it is safe to run in CI or by hand to preview.
//!
//! ```sh
//! # preview only
//! cargo run --example submit_report -- chia_network "Remote panic" poc.md "node DoS" high 1337 57
//!
//! # actually submit
//! H1_API_SUBMIT=1 cargo run --example submit_report -- chia_network "Remote panic" poc.md "node DoS" high 1337 57
//! ```

use std::process::ExitCode;

use hackerone_api::{Client, CreateHackerReport, SeverityRating};

/// Read a value from an argument, falling back to an environment variable.
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()))
}

/// Parse a severity string into the constrained enum.
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);
            }
        }
    }

    // Preview the exact payload first. This is also the CI-safe path.
    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
        }
    }
}