Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(report): support JSON output #64

Merged
merged 6 commits into from
Aug 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@ exclude = ["/assets", "/benches/fixtures"]

[features]
default = []
report = ["dep:owo-colors"]
report = ["dep:owo-colors", "dep:serde_json"]
build-binary = ["report", "dep:clap", "dep:clap-verbosity-flag", "dep:pretty_env_logger"]

[lib]
name = "keyhunter"

[[bin]]
name = "keyhunter"
required-features = ["build-binary"]
required-features = ["build-binary", "report"]

[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(codspeed)', 'cfg(tarpaulin_include)'] }
Expand All @@ -43,6 +43,7 @@ tinyvec = { version = "1.8.0", features = ["alloc", "serde", "rustc_1_40"] }
toml = { version = "0.8.14" }
ureq = { version = "2.10.1", features = ["cookies"], default-features = true }
url = { version = "2.5.2" }
serde_json = { version = "1.0.127", optional = true }

# Binary dependencies
clap = { version = "4.5.16", features = ["derive", "color"], optional = true }
Expand All @@ -53,7 +54,6 @@ pretty_env_logger = { version = "0.5.0", optional = true }
codspeed-criterion-compat = { version = "2.6.0" }
criterion = { version = "0.5.1" }
csv = { version = "1.3.0" }
serde_json = { version = "1.0.127" }

# https://doc.rust-lang.org/cargo/reference/profiles.html
[profile.dev.package]
Expand Down
6 changes: 4 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ fix:
git status

test:
cargo test --all-features
cargo nextest run --all-features
cargo test --doc --all-features
bash ./tasks/kill_8080.sh

# run tests and collect coverage. Generates tarpaulin-report.html
test-cov:
Expand All @@ -37,7 +39,7 @@ test-cov:
target/coverage/%: src tests Cargo.toml rust-toolchain.toml
mkdir -p target/coverage
RUST_BACKTRACE=1 cargo llvm-cov --all-features --$* --output-dir target/coverage
bash ./tasks/kill-8080.sh
bash ./tasks/kill_8080.sh

bench:
cargo codspeed build
Expand Down
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,16 @@ keyhunter https://example.com -H "Cookie: session-cookie=123" -H "x-another-head

This flag follows the same conventions as `curl`'s `-H` flag.

> For more information and a list of all available arguments, run `keyhunter --help`.
> For more information and a list of all available arguments, run `keyhunter
> --help`.

### Output Format

Using the `--format <format>` flag, you can specify how KeyHunter should output
its findings.
- `default`: Pretty-printed, human readable output. This is the default format.
- `json`: Print a JSON object for each finding on a separate line. This format
is really [JSON lines](https://jsonlines.org/).

## Disclaimer

Expand Down
10 changes: 6 additions & 4 deletions examples/yc_startups.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ extern crate log;
extern crate pretty_env_logger;

use keyhunter::{
report::Reporter, ApiKeyCollector, ApiKeyError, ApiKeyMessage, Config, ScriptMessage,
WebsiteWalkBuilder,
report::{Reporter, ReporterBuilder},
ApiKeyCollector, ApiKeyError, ApiKeyMessage, Config, ScriptMessage, WebsiteWalkBuilder,
};
use log::{error, info};
use miette::{miette, Context as _, Error, IntoDiagnostic as _, Result};
Expand All @@ -18,7 +18,7 @@ use std::{
time::Duration,
};

type SyncReporter = Arc<RwLock<Reporter>>;
type SyncReporter<R> = Arc<RwLock<Reporter<R>>>;

fn yc_path() -> Result<PathBuf> {
let file_path = PathBuf::from(file!()).canonicalize().into_diagnostic()?;
Expand Down Expand Up @@ -102,7 +102,9 @@ fn main() -> Result<()> {

let config = Arc::new(Config::gitleaks());

let reporter: SyncReporter = Arc::new(RwLock::new(Reporter::default().with_redacted(true)));
let reporter: SyncReporter<_> = Arc::new(RwLock::new(
ReporterBuilder::default().with_redacted(true).graphical(),
));

let yc_sites_raw = yc_file().unwrap();
let yc_reader = csv::Reader::from_reader(yc_sites_raw.as_bytes());
Expand Down
44 changes: 44 additions & 0 deletions src/cmd/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,23 +19,37 @@ use std::path::PathBuf;
use clap::{ArgAction, Parser, ValueHint};
use clap_verbosity_flag::Verbosity;
use miette::{self, IntoDiagnostic as _, Result};
use serde::{Deserialize, Serialize};

#[derive(Debug, Parser)]
#[command(author, version, about, long_about = None)]
#[command(propagate_version = true)]
pub struct Cli {
/// The URL to start crawling from.
///
/// You omit the protocol (e.g. `http://`, `https://`) and KeyHunter will
/// automatically use `https://`.
#[arg(name = "url")]
#[arg(value_hint = ValueHint::Url)]
entrypoint: String,

/// Path to a file where the output will be written.
///
/// Best used in combination with `--format json`.
#[arg(long, short)]
#[arg(value_hint = ValueHint::AnyPath)]
output: Option<PathBuf>,

#[arg(long, short)]
#[arg(default_value = "default")]
format: OutputFormat,

#[command(flatten)]
verbose: Verbosity,

/// Redact secrets from output.
///
/// Does nothing when output format is JSON.
#[arg(long, short)]
#[arg(default_value = "false")]
redact: bool,
Expand Down Expand Up @@ -68,6 +82,33 @@ pub struct Cli {
header: Vec<(String, String)>,
}

#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, clap::ValueEnum)]
#[serde(rename_all = "kebab-case")]
pub enum OutputFormat {
#[default]
Default,
Json,
}

impl OutputFormat {
#[inline]
pub fn is_default(self) -> bool {
matches!(self, Self::Default)
}
#[inline]
pub fn is_json(self) -> bool {
matches!(self, Self::Json)
}
}
impl<S: AsRef<str>> From<S> for OutputFormat {
fn from(value: S) -> Self {
match value.as_ref() {
"json" => Self::Json,
_ => Self::Default,
}
}
}

impl Cli {
const DEFAULT_MAX_WALKS: usize = 20;

Expand Down Expand Up @@ -101,6 +142,9 @@ impl Cli {
pub fn headers(&self) -> &[(String, String)] {
self.header.as_slice()
}
pub fn format(&self) -> OutputFormat {
self.format
}
}

/// Parse a single key-value pair
Expand Down
60 changes: 41 additions & 19 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,17 @@ use owo_colors::OwoColorize;
use std::{process::ExitCode, sync::Arc, thread};

use clap::Parser;
use cmd::{cli::Cli, runner::Runner};
use keyhunter::{report::Reporter, ApiKeyMessage, Config};
use cmd::{
cli::{Cli, OutputFormat},
runner::Runner,
};
use keyhunter::{
report::{
GraphicalReportHandler as KeyhunterGraphicalReportHandler, JsonReportHandler,
ReportHandler, Reporter,
},
ApiKeyMessage, Config,
};

fn main() -> Result<ExitCode> {
let cmd = Cli::parse();
Expand Down Expand Up @@ -61,7 +70,18 @@ fn main() -> Result<ExitCode> {

let start = std::time::Instant::now();

let reporter = Reporter::default().with_redacted(cmd.is_redacted());
let reporter = {
let handler: Box<dyn ReportHandler + Send + Sync> = match cmd.format() {
OutputFormat::Default => Box::new(
KeyhunterGraphicalReportHandler::default().with_redacted(cmd.is_redacted()),
),
OutputFormat::Json => Box::new(JsonReportHandler::default()),
};
Reporter::new(handler)
};
// let reporter: Reporter<_> = ReporterBuilder::default()
// .with_redacted(cmd.is_redacted())
// .graphical();
let reporter = Arc::new(reporter);
let runner = Runner::new(
Arc::new(config),
Expand All @@ -79,7 +99,7 @@ fn main() -> Result<ExitCode> {
match message {
ApiKeyMessage::Stop => break,
ApiKeyMessage::Keys(api_keys) => {
reporter.report_keys(&api_keys).unwrap();
(*reporter).report_keys(&api_keys).unwrap();
}
ApiKeyMessage::RecoverableFailure(err) => {
println!("{:?}", err);
Expand All @@ -105,21 +125,23 @@ fn main() -> Result<ExitCode> {
let num_pages = reporter.pages_crawled();
drop(reporter);

println!(
"Found {} {} across {} {} and {} {} in {:.2}{}",
num_keys.yellow(),
if num_keys == 1 { "key" } else { "keys" },
num_scripts.yellow(),
if num_scripts == 1 {
"script"
} else {
"scripts"
},
num_pages.yellow(),
if num_pages == 1 { "page" } else { "pages" },
elapsed.cyan(),
"s".cyan()
);
if cmd.format().is_default() {
println!(
"Found {} {} across {} {} and {} {} in {:.2}{}",
num_keys.yellow(),
if num_keys == 1 { "key" } else { "keys" },
num_scripts.yellow(),
if num_scripts == 1 {
"script"
} else {
"scripts"
},
num_pages.yellow(),
if num_pages == 1 { "page" } else { "pages" },
elapsed.cyan(),
"s".cyan()
);
}
if errors.is_empty() {
Ok(ExitCode::SUCCESS)
} else {
Expand Down
44 changes: 44 additions & 0 deletions src/report/builder.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
use super::{
reporters::{GraphicalReportHandler, JsonReportHandler, SyncBufWriter},
Reporter,
};
use std::io::Write;

#[derive(Debug, Default, Clone)]
pub struct ReporterBuilder {
redacted: bool,
}

impl ReporterBuilder {
#[inline]
#[must_use]
pub fn new(redacted: bool) -> Self {
Self { redacted }
}

#[inline]
#[must_use]
pub fn with_redacted(mut self, yes: bool) -> Self {
self.redacted = yes;
self
}

pub fn graphical(&self) -> Reporter<GraphicalReportHandler> {
Reporter::new(GraphicalReportHandler::new_stdout())
}

pub fn json(&self) -> Reporter<JsonReportHandler> {
Reporter::new(JsonReportHandler::default())
}

pub fn graphical_with_writer<W>(
&self,
writer: W,
) -> Reporter<GraphicalReportHandler<SyncBufWriter<W>>>
where
W: Write,
{
let handler = GraphicalReportHandler::new_buffered(writer).with_redacted(self.redacted);
Reporter::new(handler)
}
}
5 changes: 4 additions & 1 deletion src/report/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
mod graphical;
mod builder;
mod reporter;
mod reporters;
mod statistics;

pub use builder::ReporterBuilder;
pub use reporter::Reporter;
pub use reporters::{GraphicalReportHandler, JsonReportHandler, ReportHandler};
Loading
Loading