Skip to content

Commit

Permalink
Initial security-report command.
Browse files Browse the repository at this point in the history
  • Loading branch information
tmpfs committed Oct 23, 2023
1 parent 8263788 commit e4025b9
Show file tree
Hide file tree
Showing 9 changed files with 96 additions and 16 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ target
*.changes
*.sig
*.bundle
*.csv
.homebrew
tests/server_public_key.txt
geiger-report.txt
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@ mem-fs = ["sos-net/mem-fs"]
keyring = ["sos-net/keyring"]
test-utils = ["sos-net/test-utils"]

[workspace.dependencies]
csv-async = { version = "1", features = ["tokio", "with_serde"] }

[dependencies]
csv-async.workspace = true
thiserror = "1"
tracing = "0.1"
tracing-subscriber = { version = "0.3.16", features = ["env-filter"] }
Expand Down
18 changes: 16 additions & 2 deletions src/cli/sos.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use std::path::PathBuf;
use crate::{
commands::{
account, audit, changes, check, device, folder, generate_keypair,
security_report,
security_report::{self, SecurityReportFormat},
secret, shell, AccountCommand, AuditCommand, CheckCommand,
DeviceCommand, FolderCommand, SecretCommand,
},
Expand Down Expand Up @@ -86,9 +86,20 @@ pub enum Command {
},
/// Generate a security report.
SecurityReport {
/// Force overwrite if the file exists.
#[clap(short, long)]
force: bool,

/// Account name or address.
#[clap(short, long)]
account: Option<AccountRef>,

/// Output format: csv or json.
#[clap(short, long, default_value = "csv")]
output_format: SecurityReportFormat,

/// Write report to this file.
file: PathBuf,
},
/// Create, edit and delete secrets.
Secret {
Expand Down Expand Up @@ -155,7 +166,10 @@ pub async fn run() -> Result<()> {
} => generate_keypair::run(file, force, public_key).await?,
Command::SecurityReport {
account,
} => security_report::run(account, Default::default(), factory).await?,
force,
output_format,
file,
} => security_report::run(account, force, output_format, file, factory).await?,
Command::Secret { cmd } => secret::run(cmd, factory).await?,
Command::Audit { cmd } => audit::run(cmd).await?,
Command::Changes {
Expand Down
58 changes: 50 additions & 8 deletions src/commands/security_report.rs
Original file line number Diff line number Diff line change
@@ -1,27 +1,58 @@
use std::{fmt, str::FromStr, path::PathBuf};
use sos_net::{
client::{provider::ProviderFactory, user::SecurityReportOptions, hashcheck},
client::{provider::ProviderFactory, user::{SecurityReportOptions, SecurityReportRow}, hashcheck},
sdk::account::AccountRef,
};

use crate::{
helpers::{
account::resolve_user,
},
Result,
Error, Result,
};

/// Formats for writing reports.
#[derive(Default)]
#[derive(Default, Debug, Clone)]
pub enum SecurityReportFormat {
/// CSV output format.
#[default]
Csv,
/// JSON output format.
Json,
}

impl fmt::Display for SecurityReportFormat {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", match self {
Self::Csv => "csv",
Self::Json => "json",
})
}
}

impl FromStr for SecurityReportFormat {
type Err = Error;

fn from_str(s: &str) -> Result<Self> {
match s {
"csv" => Ok(Self::Csv),
"json" => Ok(Self::Json),
_ => Err(Error::UnknownReportFormat(s.to_owned())),
}
}
}

pub async fn run(
account: Option<AccountRef>,
force: bool,
format: SecurityReportFormat,
path: PathBuf,
factory: ProviderFactory,
) -> Result<()> {
if tokio::fs::try_exists(&path).await? && !force {
return Err(Error::FileExistsUseForce(path));
}

let user = resolve_user(account.as_ref(), factory, false).await?;
let mut owner = user.write().await;

Expand All @@ -42,14 +73,25 @@ pub async fn run(
report_options,
)
.await?;

let contents = match format {

let rows: Vec<SecurityReportRow<bool>> = report.into();

match format {
SecurityReportFormat::Csv => {
let mut out = csv_async::AsyncSerializer::from_writer(
tokio::fs::File::create(&path).await?
);
for row in rows {
out.serialize(&row).await?;
}
}
SecurityReportFormat::Json => {
serde_json::to_string_pretty(&report)?
let mut out = std::fs::File::create(&path)?;
serde_json::to_writer_pretty(&mut out, &rows)?;
}
};
}

println!("{}", contents);
tracing::info!(path = ?path, "wrote security report");

Ok(())
}
6 changes: 6 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,9 @@ pub enum Error {
#[error("file {0} already exists, use --force to overwrite")]
FileExistsUseForce(PathBuf),

#[error("unknown report format '{0}'")]
UnknownReportFormat(String),

/// Error generated converting to fixed length slice.
#[error(transparent)]
TryFromSlice(#[from] std::array::TryFromSliceError),
Expand Down Expand Up @@ -165,6 +168,9 @@ pub enum Error {

#[error(transparent)]
Hex(#[from] sos_net::sdk::hex::FromHexError),

#[error(transparent)]
Csv(#[from] csv_async::Error),
}

impl Error {
Expand Down
2 changes: 1 addition & 1 deletion workspace/migrate/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ license = "GPL-3.0"
repository = "https://github.com/saveoursecrets/sdk"

[dependencies]
csv-async.workspace = true
thiserror = "1"
secrecy = { version = "0.8", features = ["serde"] }
serde = { version = "1", features = ["derive"] }
Expand All @@ -24,7 +25,6 @@ async_zip = { version = "0.0.15", default-features = false, features = ["deflate
tokio-util = { version = "0.7", default-features = false, features = ["compat"] }
futures-util = "0.3"
futures = "0.3"
csv-async = { version = "1", features = ["tokio", "with_serde"] }

[dependencies.sos-sdk]
version = "0.5.19"
Expand Down
2 changes: 1 addition & 1 deletion workspace/net/src/client/user/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ pub use search_index::{ArchiveFilter, DocumentView, QueryFilter, UserIndex};
#[cfg(feature = "security-report")]
pub use security_report::{
PasswordReport, SecurityReport, SecurityReportOptions,
SecurityReportRecord,
SecurityReportRecord, SecurityReportRow,
};
pub use user_storage::{
AccountData, SecretOptions, UserStatistics, UserStorage,
Expand Down
20 changes: 16 additions & 4 deletions workspace/net/src/client/user/security_report.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ where
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SecurityReportRow<T> {
/// Folder name.
pub folder_name: String,
/// Folder identifier.
pub folder_id: VaultId,
/// Secret identifier.
Expand All @@ -35,10 +37,16 @@ pub struct SecurityReportRow<T> {
pub owner_id: Option<SecretId>,
/// Field index (when custom field).
pub field_index: Option<usize>,
/// Password security report.
#[serde(flatten)]
pub report: PasswordReport,
/// The entropy score.
pub score: u8,
/// The estimated number of guesses needed to crack the password.
pub guesses: u64,
/// The order of magnitude of guesses.
pub guesses_log10: f64,
/// Determines if the password is empty.
pub is_empty: bool,
/// Result of a database check.
#[serde(rename = "breached")]
pub database_check: T,
}

Expand All @@ -60,11 +68,15 @@ impl<T> From<SecurityReport<T>> for Vec<SecurityReportRow<T>> {
{
out.push(
SecurityReportRow {
folder_name: record.folder.name().to_owned(),
folder_id: *record.folder.id(),
secret_id: record.secret_id,
owner_id: record.owner.map(|(id, _)| id),
field_index: record.owner.map(|(_, index)| index),
report: record.report,
score: record.report.score,
guesses: record.report.guesses,
guesses_log10: record.report.guesses_log10,
is_empty: record.report.is_empty,
database_check,
}
);
Expand Down

0 comments on commit e4025b9

Please sign in to comment.