From 1248a8276257a91f89a20149fc14ebd0229c954e Mon Sep 17 00:00:00 2001 From: Lukas Pustina Date: Wed, 2 Sep 2020 10:55:36 +0200 Subject: [PATCH 1/9] Add iam primitives --- Cargo.lock | 13 ++++ aws/Cargo.toml | 1 + aws/src/iam.rs | 177 +++++++++++++++++++++++++++++++++++++++++++++++++ aws/src/lib.rs | 1 + 4 files changed, 192 insertions(+) create mode 100644 aws/src/iam.rs diff --git a/Cargo.lock b/Cargo.lock index c08d720..823d29e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -73,6 +73,7 @@ dependencies = [ "rusoto_cloudwatch 0.36.0 (registry+https://github.com/rust-lang/crates.io-index)", "rusoto_core 0.36.0 (registry+https://github.com/rust-lang/crates.io-index)", "rusoto_ec2 0.36.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rusoto_iam 0.36.0 (registry+https://github.com/rust-lang/crates.io-index)", "rusoto_kms 0.36.0 (registry+https://github.com/rust-lang/crates.io-index)", "rusoto_sts 0.36.0 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.106 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1699,6 +1700,17 @@ dependencies = [ "xml-rs 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "rusoto_iam" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "futures 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)", + "rusoto_core 0.36.0 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_urlencoded 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)", + "xml-rs 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "rusoto_kms" version = "0.36.0" @@ -2630,6 +2642,7 @@ dependencies = [ "checksum rusoto_core 0.36.0 (registry+https://github.com/rust-lang/crates.io-index)" = "18a699355ef3189e3bbf34b64ff5a31f06456b689b09d05cdb4a901dcf4406a8" "checksum rusoto_credential 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bcc8dd0f0a7e8b62f31aa23fa12fa0a7ac0e1eb52f6f4d4279d8a2ae51d8f099" "checksum rusoto_ec2 0.36.0 (registry+https://github.com/rust-lang/crates.io-index)" = "833e6433e688b642e1781458188e7d5b33f1e0e35aa0fff7c10f31c443daaf2e" +"checksum rusoto_iam 0.36.0 (registry+https://github.com/rust-lang/crates.io-index)" = "582813a0e26b45ac14305c780a8a7a092b13775ae7209f4de9c68bb6fbe5c101" "checksum rusoto_kms 0.36.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f887ab87283fa86819bd0af39ae6309b3770256ecf9a8e1ed10f3347fbf8ff4f" "checksum rusoto_sts 0.36.0 (registry+https://github.com/rust-lang/crates.io-index)" = "cd5a6ebb596b8739ab7b3d8d98900d6633712b4e79d2540fa7198a82c2605e4e" "checksum rust-argon2 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2bc8af4bda8e1ff4932523b94d3dd20ee30a87232323eda55903ffd71d2fb017" diff --git a/aws/Cargo.toml b/aws/Cargo.toml index 6a95a2d..37838ae 100644 --- a/aws/Cargo.toml +++ b/aws/Cargo.toml @@ -20,6 +20,7 @@ rusoto_autoscaling = "0.36" rusoto_cloudwatch = "0.36" rusoto_core = "0.36" rusoto_ec2 = "0.36" +rusoto_iam = "0.36" rusoto_kms = "0.36" rusoto_sts = "0.36" serde = "1" diff --git a/aws/src/iam.rs b/aws/src/iam.rs new file mode 100644 index 0000000..cdd73a9 --- /dev/null +++ b/aws/src/iam.rs @@ -0,0 +1,177 @@ +use crate::AwsClientConfig; +use failure::{Error, err_msg}; +use log::{debug, warn}; +use rusoto_iam::{Iam, IamClient, ListUsersRequest, ListAccessKeysRequest, GetAccessKeyLastUsedRequest}; +use chrono::{DateTime, Utc}; +use std::str::FromStr; +use std::convert::TryFrom; + +#[derive(Debug)] +pub struct User { + password_last_used: Option>, + user_id: String, + user_name: String, + path: String, +} + +impl From for User { + fn from(user: rusoto_iam::User) -> Self { + let password_last_used = user.password_last_used + .and_then(|x| DateTime::parse_from_rfc3339(&x).ok()) + .map(|x| x.with_timezone(&Utc)); + + User { + password_last_used, + user_id: user.user_id, + user_name: user.user_name, + path: user.path, + } + } +} + +pub fn list_users(aws_client_config: &AwsClientConfig) -> Result, Error> { + debug!("List users"); + + let credentials_provider = aws_client_config.credentials_provider.clone(); + let http_client = aws_client_config.http_client.clone(); + let iam = IamClient::new_with(http_client, credentials_provider, aws_client_config.region.clone()); + + let request = ListUsersRequest { + marker: None, + max_items: Some(100), + path_prefix: None, + }; + let res = iam.list_users(request).sync(); + debug!("Finished list user request; success={}.", res.is_ok()); + let res = res?; + + if log::max_level() >= log::Level::Warn && res.is_truncated.is_some() && res.is_truncated.unwrap() { + warn!("List users: Result is truncated."); + } + + let res: Vec = res.users.into_iter() + .map(Into::into) + .collect(); + + Ok(res) +} + +#[derive(Debug)] +pub struct AccessKeyMetadata { + access_key_id: String, + create_date: DateTime, + status: AccessKeyMetadataStatus, + user_name: String, +} + +impl TryFrom for AccessKeyMetadata { + type Error = Error; + + fn try_from(value: rusoto_iam::AccessKeyMetadata) -> Result { + let access_key_id = value.access_key_id.ok_or_else(|| err_msg("no access key provided"))?; + let create_date = value.create_date + .ok_or_else(|| err_msg("no create date provided")) + .and_then(|x| + DateTime::parse_from_rfc3339(&x).map_err(|_| err_msg("failed to parse create date")) + ) + .map(|x| x.with_timezone(&Utc))?; + let status = value.status + .ok_or_else(|| err_msg("no status provided")) + .and_then(|x| AccessKeyMetadataStatus::from_str(&x))?; + let user_name = value.user_name.ok_or_else(|| err_msg("no user name provided"))?; + + Ok(AccessKeyMetadata { + access_key_id, + create_date, + status, + user_name + }) + } +} + +#[derive(Debug, Clone, Copy)] +pub enum AccessKeyMetadataStatus { + Active, + Inactive, +} + +impl FromStr for AccessKeyMetadataStatus { + type Err = Error; + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "active" => Ok(AccessKeyMetadataStatus::Active), + "inactive" => Ok(AccessKeyMetadataStatus::Inactive), + _ => Err(err_msg("failed to parse access key status")) + } + } +} + +pub fn list_access_keys_for_user(aws_client_config: &AwsClientConfig, user_name: String) -> Result, Error> { + debug!("List access keys for user '{}'", &user_name); + + let credentials_provider = aws_client_config.credentials_provider.clone(); + let http_client = aws_client_config.http_client.clone(); + let iam = IamClient::new_with(http_client, credentials_provider, aws_client_config.region.clone()); + + let request = ListAccessKeysRequest { + marker: None, + max_items: Some(100), + user_name: Some(user_name.clone()), + }; + let res = iam.list_access_keys(request).sync(); + debug!("Finished list access keys request for user '{}'; success={}.", &user_name, res.is_ok()); + let res = res?; + + if log::max_level() >= log::Level::Warn && res.is_truncated.is_some() && res.is_truncated.unwrap() { + warn!("List users: Result is truncated."); + } + + let res: Vec> = res.access_key_metadata.into_iter() + .map(TryFrom::try_from) + .collect(); + let res: Result, Error> = res.into_iter().collect(); + + res +} + +#[derive(Debug)] +pub struct AccessKeyLastUsed { + last_used_date: DateTime, + region: String, + service_name: String, +} + +impl TryFrom for AccessKeyLastUsed { + type Error = Error; + + fn try_from(value: rusoto_iam::AccessKeyLastUsed) -> Result { + let last_used_date = DateTime::parse_from_rfc3339(&value.last_used_date) + .map_err(|_| err_msg("failed to parse create date")) + .map(|x| x.with_timezone(&Utc))?; + + Ok(AccessKeyLastUsed { + last_used_date, + region: value.region, + service_name: value.service_name, + }) + } +} + +pub fn list_access_last_used(aws_client_config: &AwsClientConfig, access_key_id: String) -> Result { + debug!("Get access key last used for key '{}'", &access_key_id); + + let credentials_provider = aws_client_config.credentials_provider.clone(); + let http_client = aws_client_config.http_client.clone(); + let iam = IamClient::new_with(http_client, credentials_provider, aws_client_config.region.clone()); + + let request = GetAccessKeyLastUsedRequest { + access_key_id: access_key_id.clone(), + }; + + let res = iam.get_access_key_last_used(request).sync(); + debug!("Finished get access key last used for key '{}'; success={}.", &access_key_id, res.is_ok()); + let res = res?.access_key_last_used.ok_or_else(|| err_msg("no result received"))?; + + AccessKeyLastUsed::try_from(res) +} diff --git a/aws/src/lib.rs b/aws/src/lib.rs index e476269..a4b6e20 100644 --- a/aws/src/lib.rs +++ b/aws/src/lib.rs @@ -5,6 +5,7 @@ use std::sync::Arc; pub mod auth; pub mod cloudwatch; pub mod ec2; +pub mod iam; pub mod kms; #[derive(Debug, Fail)] From 799c8d52165dcca109f8b865731fd0ae6c8c77a7 Mon Sep 17 00:00:00 2001 From: Lukas Pustina Date: Wed, 2 Sep 2020 15:09:51 +0200 Subject: [PATCH 2/9] Init AwsCredential Check --- Cargo.lock | 31 +++++++++++ Cargo.toml | 1 + README.asciidoc | 3 ++ aws/src/iam.rs | 32 +++++------ aws/src/lib.rs | 5 +- credentials-watchtower/Cargo.toml | 52 ++++++++++++++++++ credentials-watchtower/Makefile | 6 +++ credentials-watchtower/README.md | 49 +++++++++++++++++ credentials-watchtower/build.rs | 12 +++++ .../src/bin/check_credentials.rs | 50 +++++++++++++++++ .../src/check_credentials.rs | 54 +++++++++++++++++++ credentials-watchtower/src/lib.rs | 1 + 12 files changed, 279 insertions(+), 17 deletions(-) create mode 100644 credentials-watchtower/Cargo.toml create mode 100644 credentials-watchtower/Makefile create mode 100644 credentials-watchtower/README.md create mode 100644 credentials-watchtower/build.rs create mode 100644 credentials-watchtower/src/bin/check_credentials.rs create mode 100644 credentials-watchtower/src/check_credentials.rs create mode 100644 credentials-watchtower/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 823d29e..ae56018 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -411,6 +411,37 @@ dependencies = [ "cfg-if 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "credentials-watchtower" +version = "0.0.1" +dependencies = [ + "aws 0.0.3", + "bosun 0.0.2", + "chrono 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)", + "clams 0.0.13 (registry+https://github.com/rust-lang/crates.io-index)", + "clams-derive 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)", + "dirs 3.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "env_logger 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", + "failure 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", + "failure_derive 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", + "lambda 0.1.0", + "lambda_runtime 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "linreg 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "prettytable-rs 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", + "reqwest 0.9.24 (registry+https://github.com/rust-lang/crates.io-index)", + "rusoto_core 0.36.0 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.106 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive 1.0.106 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.51 (registry+https://github.com/rust-lang/crates.io-index)", + "spectral 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "structopt 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)", + "testing 0.0.1", + "toml 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)", + "vergen 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "crossbeam-deque" version = "0.7.3" diff --git a/Cargo.toml b/Cargo.toml index 80390d0..f8656ab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ members = [ "aws-scaletower", "aws-watchtower", "bosun", + "credentials-watchtower", "lambda", "testing", ] diff --git a/README.asciidoc b/README.asciidoc index 4bef3d4..4170589 100644 --- a/README.asciidoc +++ b/README.asciidoc @@ -10,3 +10,6 @@ image:https://dev.azure.com/centerdevice/ceres-lambda/_apis/build/status/CenterD - If ASG life cycle events indicates an EC2 instance is getting scaled-down, we want a corresponding silence for the durAtion of `asg.scaledown_silence_duration`. In this way, we prevent unknown alarms from hitting the Slack channels. Unfortunately, these events may arrive too late and thus, the silence is set too late in which case the unknown bursts hit us anyway. The reason behind this is that -- according to AWS SAs -- ASGs trigger these events only after failing health checks. For this reason, we have a second mechanism based on the shutting-down state of EC2 instances. If an EC2StateChangeEvent with state `shutting-down` is received and the corresponding EC2 instance is part of an autoscaling group, we silence alarms, especially unknown bursts, for the duration of `ec2.scaledown_silence_duration`. This duration is much smaller then `asg.scaledown_silence_duration`. The idea is that in case an ASG scales down an EC2 instance, we receive this event much earlier than the ASG life cycle event. But this event might have other reasons. So we set the silence for a short period of time until either the ASG life cycle event sets a long silence or this silence expires and triggers alarms. * `aws-scaletower` -- Checks EC2 instances for running out of IO burst balance. If a burst balance below a threshold or with predicted time until the balance is exhausted is identified, the EC2 instance will be terminated and automatically replaced by the corresponding autoscaling group. + +* `credentials-watchtower` -- Checks credentials at DUO and AWS for last time used. If credentials with a longer period if inactivity are identified, those credentials will be destroyed. + diff --git a/aws/src/iam.rs b/aws/src/iam.rs index cdd73a9..0e29593 100644 --- a/aws/src/iam.rs +++ b/aws/src/iam.rs @@ -1,17 +1,17 @@ use crate::AwsClientConfig; use failure::{Error, err_msg}; use log::{debug, warn}; -use rusoto_iam::{Iam, IamClient, ListUsersRequest, ListAccessKeysRequest, GetAccessKeyLastUsedRequest}; +use rusoto_iam::{Iam, IamClient, ListUsersRequest, ListAccessKeysRequest, GetAccessKeyLastUsedRequest, ListUsersError}; use chrono::{DateTime, Utc}; use std::str::FromStr; use std::convert::TryFrom; -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct User { - password_last_used: Option>, - user_id: String, - user_name: String, - path: String, + pub password_last_used: Option>, + pub user_id: String, + pub user_name: String, + pub path: String, } impl From for User { @@ -43,7 +43,7 @@ pub fn list_users(aws_client_config: &AwsClientConfig) -> Result, Erro }; let res = iam.list_users(request).sync(); debug!("Finished list user request; success={}.", res.is_ok()); - let res = res?; + let res = res.expect("failed to list users"); if log::max_level() >= log::Level::Warn && res.is_truncated.is_some() && res.is_truncated.unwrap() { warn!("List users: Result is truncated."); @@ -56,12 +56,12 @@ pub fn list_users(aws_client_config: &AwsClientConfig) -> Result, Erro Ok(res) } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct AccessKeyMetadata { - access_key_id: String, - create_date: DateTime, - status: AccessKeyMetadataStatus, - user_name: String, + pub access_key_id: String, + pub create_date: DateTime, + pub status: AccessKeyMetadataStatus, + pub user_name: String, } impl TryFrom for AccessKeyMetadata { @@ -135,11 +135,11 @@ pub fn list_access_keys_for_user(aws_client_config: &AwsClientConfig, user_name: res } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct AccessKeyLastUsed { - last_used_date: DateTime, - region: String, - service_name: String, + pub last_used_date: DateTime, + pub region: String, + pub service_name: String, } impl TryFrom for AccessKeyLastUsed { diff --git a/aws/src/lib.rs b/aws/src/lib.rs index a4b6e20..eb660d0 100644 --- a/aws/src/lib.rs +++ b/aws/src/lib.rs @@ -24,8 +24,11 @@ pub struct AwsClientConfig { impl AwsClientConfig { pub fn new() -> Result { + AwsClientConfig::with_region(Region::EuCentral1) + } + + pub fn with_region(region: Region) -> Result { let credential_provider = auth::create_provider()?; - let region = Region::EuCentral1; AwsClientConfig::with_credentials_provider_and_region(credential_provider, region) } diff --git a/credentials-watchtower/Cargo.toml b/credentials-watchtower/Cargo.toml new file mode 100644 index 0000000..0ae515a --- /dev/null +++ b/credentials-watchtower/Cargo.toml @@ -0,0 +1,52 @@ +[package] +name = "credentials-watchtower" +version = "0.0.1" +authors = ["Lukas Pustina "] +build = "build.rs" + +edition = "2018" + + +[[bin]] +path = "src/bin/check_credentials.rs" +name = "check_credentials" + +[lib] +name = "credentials_watchtower" +path = "src/lib.rs" + +[dependencies] +aws = { version = "0.0.3", path = "../aws" } +clams = "0.0.13" +clams-derive = "^0.0.4" +bosun = { version = "0.0.2", path = "../bosun" } +chrono = { version = "0.4", features = ["serde"] } +dirs = "3" +env_logger = "0.6" +failure = "0.1" +failure_derive = "0.1" +lambda_runtime = "0.1" +lambda = { version = "0.1.0", path = "../lambda" } +lazy_static = "1.2" +linreg = "0.2" +log = "0.4" +prettytable-rs = "0.8" +reqwest = "0.9" +rusoto_core = "0.36" +serde = "1" +serde_derive = "1" +serde_json = "1" +structopt = "0.2" +toml = "0.4" + +[dev-dependencies] +spectral = "^0.6" +testing = { version = "0.0.1", path = "../testing" } + +[build-dependencies] +vergen = "3" + +# Enable Debug Symbols in Release build +#[profile.release] +#debug = true + diff --git a/credentials-watchtower/Makefile b/credentials-watchtower/Makefile new file mode 100644 index 0000000..819534c --- /dev/null +++ b/credentials-watchtower/Makefile @@ -0,0 +1,6 @@ +FUNC_NAME = aws-scaletower +FUNC_NAME_BIN = $(FUNC_NAME) + +include ../.includes/Makefile.in + + diff --git a/credentials-watchtower/README.md b/credentials-watchtower/README.md new file mode 100644 index 0000000..62174fa --- /dev/null +++ b/credentials-watchtower/README.md @@ -0,0 +1,49 @@ +# AWS-Scaletower + + +## Configuration + +### Format + +```toml +[bosun] +host = '' +user = ' minutes to compute linear regression +look_back_min = +// Enable linear regression to compute ETA for when instance runs out of burts +use_linear_regression = +// Burst balance limit after which the instance will be termianted +burst_balance_limit = +// Limit in min after which the instance will be termianted +eta_limit_min = +// Enabled instance termination +terminate = +``` + +### Validate Configuration + +This crate contains a executable that validates an encrypted configuration file called `validate-config-scaletower`. Please check the help information for details. For decryption valid AWS credentials in environment variables are required. + +In this example, the encrypted as well as the decrypted configurations are printed and checked: + +```Bash +cargo run --bin validate-config-scaletower ~INFRA/AWS/staging/logimon/terraform/resources/lambda/packages/config_enc_aws-scaletower.conf -vv +``` + +You can set the AWS credentials for example using `aws-switchrole` -- see below. In this case, don't forget to paste and eval. + +```Bash +aws-switchrole --profile staging@cd --copy +``` + diff --git a/credentials-watchtower/build.rs b/credentials-watchtower/build.rs new file mode 100644 index 0000000..142fa1e --- /dev/null +++ b/credentials-watchtower/build.rs @@ -0,0 +1,12 @@ +extern crate vergen; + +use vergen::{generate_cargo_keys, ConstantsFlags}; + +fn main() { + // Setup the flags, toggling off the 'SEMVER_FROM_CARGO_PKG' flag + let mut flags = ConstantsFlags::all(); + flags.toggle(ConstantsFlags::SEMVER); + + // Generate the 'cargo:' key output + generate_cargo_keys(ConstantsFlags::all()).expect("Unable to generate the cargo keys!"); +} diff --git a/credentials-watchtower/src/bin/check_credentials.rs b/credentials-watchtower/src/bin/check_credentials.rs new file mode 100644 index 0000000..5409c6a --- /dev/null +++ b/credentials-watchtower/src/bin/check_credentials.rs @@ -0,0 +1,50 @@ +use aws::{ + auth::{create_provider_with_assuem_role, StsAssumeRoleSessionCredentialsProviderConfig}, + AwsClientConfig, +}; +use rusoto_core::Region; +use chrono::prelude::*; +use prettytable::{format, Cell, Row, Table}; +use credentials_watchtower::check_credentials::{check_aws_credentials, CredentialCheck, AwsCredential}; + +fn main() { + env_logger::init(); + let aws_client_config = AwsClientConfig::with_region(Region::UsEast1).expect("Failed to create AWS client config"); + + let credentials = check_aws_credentials(&aws_client_config) + .expect("failed to load credentials"); + + let mut table = Table::new(); + table.set_format(*format::consts::FORMAT_NO_LINESEP_WITH_TITLE); + table.set_titles(Row::new(vec![ + Cell::new("Service"), + Cell::new("User"), + Cell::new("Credential Type"), + Cell::new("Last Time Used"), + ])); + + for c in &credentials { + let row = match c { + CredentialCheck::Aws { credential } => aws_credential_to_row(credential), + }; + table.add_row(row); + } + + table.printstd(); +} + +fn aws_credential_to_row(credential: &AwsCredential) -> Row { + let service = "AWS"; + let user = format!("{} ({})", credential.user_name, credential.user_name); + let credential_type = format!("{:?}", credential.credential); + let last_time_used = credential.last_used + .map(|x| x.to_rfc3339()) + .unwrap_or_else(|| "-".to_string()); + + Row::new(vec![ + Cell::new(service), + Cell::new(&user), + Cell::new(&credential_type), + Cell::new(&last_time_used), + ]) +} \ No newline at end of file diff --git a/credentials-watchtower/src/check_credentials.rs b/credentials-watchtower/src/check_credentials.rs new file mode 100644 index 0000000..8367e87 --- /dev/null +++ b/credentials-watchtower/src/check_credentials.rs @@ -0,0 +1,54 @@ +use failure::Error; + +use aws::AwsClientConfig; +use aws::iam; +use chrono::{DateTime, Utc}; +use aws::iam::User; + +#[derive(Debug)] +pub enum CredentialCheck { + Aws { credential: AwsCredential }, +} + +#[derive(Debug)] +pub struct AwsCredential { + pub user_id: String, + pub user_name: String, + pub credential: CredentialType, + pub last_used: Option>, +} + +impl From for AwsCredential { + fn from(user: iam::User) -> Self { + AwsCredential { + user_id: user.user_id, + user_name: user.user_name, + credential: CredentialType::Password, + last_used: user.password_last_used, + } + } +} + +#[derive(Debug)] +pub enum CredentialType { + Password, + ApiKey, +} + +pub fn check_aws_credentials( + aws_client_config: &AwsClientConfig, +) -> Result, Error> { + + let users = iam::list_users(&aws_client_config)?; + + let mut credentials: Vec = Vec::new(); + let user_credentials: Vec = users.clone().into_iter() + .map(Into::into) + .collect(); + + user_credentials.into_iter() + .map(|x| CredentialCheck::Aws { credential: x}) + .map(|x| credentials.push(x)).collect::>(); + + Ok(credentials) +} \ No newline at end of file diff --git a/credentials-watchtower/src/lib.rs b/credentials-watchtower/src/lib.rs new file mode 100644 index 0000000..66aa9e7 --- /dev/null +++ b/credentials-watchtower/src/lib.rs @@ -0,0 +1 @@ +pub mod check_credentials; \ No newline at end of file From e10ae4e11eadc5ebba594287cbff11738550be82 Mon Sep 17 00:00:00 2001 From: Lukas Pustina Date: Wed, 2 Sep 2020 20:32:17 +0200 Subject: [PATCH 3/9] Add check_credentials bin --- aws/src/iam.rs | 16 ++++---- .../src/bin/check_credentials.rs | 13 +++++-- .../src/check_credentials.rs | 39 ++++++++++++++++--- 3 files changed, 51 insertions(+), 17 deletions(-) diff --git a/aws/src/iam.rs b/aws/src/iam.rs index 0e29593..c036b63 100644 --- a/aws/src/iam.rs +++ b/aws/src/iam.rs @@ -1,7 +1,7 @@ use crate::AwsClientConfig; use failure::{Error, err_msg}; use log::{debug, warn}; -use rusoto_iam::{Iam, IamClient, ListUsersRequest, ListAccessKeysRequest, GetAccessKeyLastUsedRequest, ListUsersError}; +use rusoto_iam::{Iam, IamClient, ListUsersRequest, ListAccessKeysRequest, GetAccessKeyLastUsedRequest}; use chrono::{DateTime, Utc}; use std::str::FromStr; use std::convert::TryFrom; @@ -137,20 +137,22 @@ pub fn list_access_keys_for_user(aws_client_config: &AwsClientConfig, user_name: #[derive(Debug, Clone)] pub struct AccessKeyLastUsed { + pub user_name: String, + pub access_key_id: String, pub last_used_date: DateTime, pub region: String, pub service_name: String, } -impl TryFrom for AccessKeyLastUsed { - type Error = Error; - - fn try_from(value: rusoto_iam::AccessKeyLastUsed) -> Result { +impl AccessKeyLastUsed { + fn try_from(user_name: String, access_key_id: String, value: rusoto_iam::AccessKeyLastUsed) -> Result { let last_used_date = DateTime::parse_from_rfc3339(&value.last_used_date) .map_err(|_| err_msg("failed to parse create date")) .map(|x| x.with_timezone(&Utc))?; Ok(AccessKeyLastUsed { + user_name, + access_key_id, last_used_date, region: value.region, service_name: value.service_name, @@ -158,7 +160,7 @@ impl TryFrom for AccessKeyLastUsed { } } -pub fn list_access_last_used(aws_client_config: &AwsClientConfig, access_key_id: String) -> Result { +pub fn list_access_last_used(aws_client_config: &AwsClientConfig, user_name: String, access_key_id: String) -> Result { debug!("Get access key last used for key '{}'", &access_key_id); let credentials_provider = aws_client_config.credentials_provider.clone(); @@ -173,5 +175,5 @@ pub fn list_access_last_used(aws_client_config: &AwsClientConfig, access_key_id: debug!("Finished get access key last used for key '{}'; success={}.", &access_key_id, res.is_ok()); let res = res?.access_key_last_used.ok_or_else(|| err_msg("no result received"))?; - AccessKeyLastUsed::try_from(res) + AccessKeyLastUsed::try_from(user_name, access_key_id, res) } diff --git a/credentials-watchtower/src/bin/check_credentials.rs b/credentials-watchtower/src/bin/check_credentials.rs index 5409c6a..ac737cd 100644 --- a/credentials-watchtower/src/bin/check_credentials.rs +++ b/credentials-watchtower/src/bin/check_credentials.rs @@ -1,7 +1,4 @@ -use aws::{ - auth::{create_provider_with_assuem_role, StsAssumeRoleSessionCredentialsProviderConfig}, - AwsClientConfig, -}; +use aws::AwsClientConfig; use rusoto_core::Region; use chrono::prelude::*; use prettytable::{format, Cell, Row, Table}; @@ -21,6 +18,7 @@ fn main() { Cell::new("User"), Cell::new("Credential Type"), Cell::new("Last Time Used"), + Cell::new("Last Usage [days]"), ])); for c in &credentials { @@ -40,11 +38,18 @@ fn aws_credential_to_row(credential: &AwsCredential) -> Row { let last_time_used = credential.last_used .map(|x| x.to_rfc3339()) .unwrap_or_else(|| "-".to_string()); + let last_usage = if let Some(last_used) = credential.last_used { + let since = Utc::now() - last_used; + format!("{}", since.num_days()) + } else { + "-".to_string() + }; Row::new(vec![ Cell::new(service), Cell::new(&user), Cell::new(&credential_type), Cell::new(&last_time_used), + Cell::new(&last_usage).style_spec("r"), ]) } \ No newline at end of file diff --git a/credentials-watchtower/src/check_credentials.rs b/credentials-watchtower/src/check_credentials.rs index 8367e87..e72e95d 100644 --- a/credentials-watchtower/src/check_credentials.rs +++ b/credentials-watchtower/src/check_credentials.rs @@ -3,7 +3,7 @@ use failure::Error; use aws::AwsClientConfig; use aws::iam; use chrono::{DateTime, Utc}; -use aws::iam::User; +use aws::iam::AccessKeyLastUsed; #[derive(Debug)] pub enum CredentialCheck { @@ -12,7 +12,7 @@ pub enum CredentialCheck { #[derive(Debug)] pub struct AwsCredential { - pub user_id: String, + pub id: String, pub user_name: String, pub credential: CredentialType, pub last_used: Option>, @@ -21,7 +21,7 @@ pub struct AwsCredential { impl From for AwsCredential { fn from(user: iam::User) -> Self { AwsCredential { - user_id: user.user_id, + id: user.user_id, user_name: user.user_name, credential: CredentialType::Password, last_used: user.password_last_used, @@ -29,6 +29,17 @@ impl From for AwsCredential { } } +impl From for AwsCredential { + fn from(key: AccessKeyLastUsed) -> Self { + AwsCredential { + id: key.access_key_id, + user_name: key.user_name, + credential: CredentialType::ApiKey, + last_used: Some(key.last_used_date), + } + } +} + #[derive(Debug)] pub enum CredentialType { Password, @@ -42,13 +53,29 @@ pub fn check_aws_credentials( let users = iam::list_users(&aws_client_config)?; let mut credentials: Vec = Vec::new(); + let user_credentials: Vec = users.clone().into_iter() .map(Into::into) .collect(); - - user_credentials.into_iter() + let _ = user_credentials.into_iter() .map(|x| CredentialCheck::Aws { credential: x}) .map(|x| credentials.push(x)).collect::>(); + let access_keys: Vec<_> = users.into_iter() + .map(|x| x.user_name) + .map(|x| { + iam::list_access_keys_for_user(&aws_client_config, x) + }) + .flatten() + .flatten() + .map(|x| iam::list_access_last_used(&aws_client_config, x.user_name.clone(), x.access_key_id)) + .filter(|x| x.is_ok()) + .flatten() + .collect(); + let _ = access_keys.into_iter() + .map(Into::into) + .map(|x| CredentialCheck::Aws {credential: x}) + .map(|x| credentials.push(x)).collect::>(); + Ok(credentials) -} \ No newline at end of file +} From 3744f6c5b53fd8cd9b0f033a82f1bbac73f34b8c Mon Sep 17 00:00:00 2001 From: Lukas Pustina Date: Thu, 3 Sep 2020 08:26:46 +0200 Subject: [PATCH 4/9] Add check for expiration --- .../src/bin/check_credentials.rs | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/credentials-watchtower/src/bin/check_credentials.rs b/credentials-watchtower/src/bin/check_credentials.rs index ac737cd..1e4e71e 100644 --- a/credentials-watchtower/src/bin/check_credentials.rs +++ b/credentials-watchtower/src/bin/check_credentials.rs @@ -1,8 +1,9 @@ -use aws::AwsClientConfig; -use rusoto_core::Region; use chrono::prelude::*; -use prettytable::{format, Cell, Row, Table}; -use credentials_watchtower::check_credentials::{check_aws_credentials, CredentialCheck, AwsCredential}; +use prettytable::{Cell, format, Row, Table}; +use rusoto_core::Region; + +use aws::AwsClientConfig; +use credentials_watchtower::check_credentials::{AwsCredential, check_aws_credentials, CredentialCheck}; fn main() { env_logger::init(); @@ -19,6 +20,8 @@ fn main() { Cell::new("Credential Type"), Cell::new("Last Time Used"), Cell::new("Last Usage [days]"), + Cell::new("> 2 Months"), + Cell::new("> 6 Months"), ])); for c in &credentials { @@ -38,11 +41,15 @@ fn aws_credential_to_row(credential: &AwsCredential) -> Row { let last_time_used = credential.last_used .map(|x| x.to_rfc3339()) .unwrap_or_else(|| "-".to_string()); - let last_usage = if let Some(last_used) = credential.last_used { + let (last_usage, last_usage_is_2_months, last_usage_is_6_months) = if let Some(last_used) = credential.last_used { let since = Utc::now() - last_used; - format!("{}", since.num_days()) + ( + format!("{}", since.num_days()), + format!("{}", since.num_weeks() > 8), + format!("{}", since.num_weeks() > 24), + ) } else { - "-".to_string() + ("-".to_string(), "-".to_string(), "-".to_string()) }; Row::new(vec![ @@ -51,5 +58,7 @@ fn aws_credential_to_row(credential: &AwsCredential) -> Row { Cell::new(&credential_type), Cell::new(&last_time_used), Cell::new(&last_usage).style_spec("r"), + Cell::new(&last_usage_is_2_months).style_spec("c"), + Cell::new(&last_usage_is_6_months).style_spec("c"), ]) } \ No newline at end of file From cd7eed8a4f1baeaafeee427a58cebee18dc3d47d Mon Sep 17 00:00:00 2001 From: Lukas Pustina Date: Thu, 3 Sep 2020 11:26:12 +0200 Subject: [PATCH 5/9] Init Duo crate --- Cargo.lock | 174 ++++++++++++++++++++++++++++++++++++++++++----- Cargo.toml | 1 + duo/Cargo.toml | 29 ++++++++ duo/src/lib.rs | 180 +++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 366 insertions(+), 18 deletions(-) create mode 100644 duo/Cargo.toml create mode 100644 duo/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index ae56018..3bd8e9c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -156,7 +156,7 @@ name = "backtrace-sys" version = "0.1.35" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "cc 1.0.50 (registry+https://github.com/rust-lang/crates.io-index)", + "cc 1.0.59 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -234,6 +234,11 @@ dependencies = [ "serde 1.0.106 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "bumpalo" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "byte-tools" version = "0.2.0" @@ -256,7 +261,7 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.50" +version = "1.0.59" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] @@ -556,9 +561,9 @@ name = "displaydoc" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "proc-macro2 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.19 (registry+https://github.com/rust-lang/crates.io-index)", "quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -566,6 +571,26 @@ name = "dtoa" version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "duo" +version = "0.0.1" +dependencies = [ + "base64 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", + "chrono 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)", + "env_logger 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", + "failure 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", + "failure_derive 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", + "hex 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "reqwest 0.9.24 (registry+https://github.com/rust-lang/crates.io-index)", + "ring 0.16.15 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.106 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive 1.0.106 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.51 (registry+https://github.com/rust-lang/crates.io-index)", + "spectral 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "testing 0.0.1", +] + [[package]] name = "either" version = "1.5.3" @@ -626,9 +651,9 @@ name = "failure_derive" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "proc-macro2 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.19 (registry+https://github.com/rust-lang/crates.io-index)", "quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)", "synstructure 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -773,6 +798,11 @@ name = "hex" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "hex" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "hmac" version = "0.5.0" @@ -928,6 +958,14 @@ name = "itoa" version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "js-sys" +version = "0.3.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "wasm-bindgen 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "kernel32-sys" version = "0.2.2" @@ -1230,6 +1268,11 @@ dependencies = [ "libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "once_cell" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "openssl" version = "0.10.29" @@ -1254,7 +1297,7 @@ version = "0.9.55" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", - "cc 1.0.50 (registry+https://github.com/rust-lang/crates.io-index)", + "cc 1.0.59 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)", "pkg-config 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)", "vcpkg 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1357,7 +1400,7 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.10" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1401,7 +1444,7 @@ name = "quote" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "proc-macro2 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.19 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1657,6 +1700,20 @@ dependencies = [ "winreg 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "ring" +version = "0.16.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cc 1.0.59 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)", + "once_cell 1.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "spin 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", + "untrusted 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "web-sys 0.3.44 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "rusoto_autoscaling" version = "0.36.0" @@ -1866,9 +1923,9 @@ name = "serde_derive" version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "proc-macro2 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.19 (registry+https://github.com/rust-lang/crates.io-index)", "quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1941,6 +1998,11 @@ dependencies = [ "num 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "string" version = "0.2.1" @@ -2006,10 +2068,10 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.17" +version = "1.0.39" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "proc-macro2 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.19 (registry+https://github.com/rust-lang/crates.io-index)", "quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -2019,9 +2081,9 @@ name = "synstructure" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "proc-macro2 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.19 (registry+https://github.com/rust-lang/crates.io-index)", "quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)", "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -2367,6 +2429,11 @@ name = "unicode-xid" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "url" version = "1.7.2" @@ -2439,6 +2506,64 @@ name = "wasi" version = "0.9.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "wasm-bindgen" +version = "0.2.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cfg-if 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)", + "wasm-bindgen-macro 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bumpalo 3.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.19 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)", + "wasm-bindgen-shared 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", + "wasm-bindgen-macro-support 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 1.0.19 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)", + "wasm-bindgen-backend 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)", + "wasm-bindgen-shared 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.67" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "web-sys" +version = "0.3.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "js-sys 0.3.44 (registry+https://github.com/rust-lang/crates.io-index)", + "wasm-bindgen 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "winapi" version = "0.2.8" @@ -2520,10 +2645,11 @@ dependencies = [ "checksum blake2b_simd 0.5.10 (registry+https://github.com/rust-lang/crates.io-index)" = "d8fb2d74254a3a0b5cac33ac9f8ed0e44aa50378d9dbb2e5d83bd21ed1dc2c8a" "checksum block-buffer 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a076c298b9ecdb530ed9d967e74a6027d6a7478924520acddcddc24c1c8ab3ab" "checksum bstr 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)" = "31accafdb70df7871592c058eca3985b71104e15ac32f64706022c58867da931" +"checksum bumpalo 3.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2e8c087f005730276d1096a652e92a8bacee2e2472bcc9715a74d2bec38b5820" "checksum byte-tools 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "560c32574a12a89ecd91f5e742165893f86e3ab98d21f8ea548658eb9eef5f40" "checksum byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" "checksum bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)" = "206fdffcfa2df7cbe15601ef46c813fce0965eb3286db6b56c583b814b51c81c" -"checksum cc 1.0.50 (registry+https://github.com/rust-lang/crates.io-index)" = "95e28fa049fda1c330bcf9d723be7663a899c4679724b34c81e9f5a326aab8cd" +"checksum cc 1.0.59 (registry+https://github.com/rust-lang/crates.io-index)" = "66120af515773fb005778dc07c261bd201ec8ce50bd6e7144c927753fe013381" "checksum cfg-if 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "b486ce3ccf7ffd79fdeb678eac06a9e6c09fc88d33836340becb8fffe87c5e33" "checksum chrono 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)" = "80094f509cf8b5ae86a4966a39b3ff66cd7e2a3e594accec3743ff3fabeab5b2" "checksum clams 0.0.13 (registry+https://github.com/rust-lang/crates.io-index)" = "4a1c9ecb3038934953388c9451ff11d8df8781bc603587a91b2b9e053fdae58a" @@ -2578,6 +2704,7 @@ dependencies = [ "checksum heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205" "checksum hermit-abi 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)" = "8a0d737e0f947a1864e93d33fdef4af8445a00d1ed8dc0c8ddb73139ea6abf15" "checksum hex 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "805026a5d0141ffc30abb3be3173848ad46a1b1664fe632428479619a3644d77" +"checksum hex 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "644f9158b2f133fd50f5fb3242878846d9eb792e445c893805ff0e3824006e35" "checksum hmac 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "44f3bdb08579d99d7dc761c0e266f13b5f2ab8c8c703b9fc9ef333cd8f48f55e" "checksum http 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)" = "d6ccf5ede3a895d8856620237b2f02972c1bbc78d2965ad7fe8838d4a0ed41f0" "checksum http-body 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6741c859c1b2463a423a1dbce98d418e6c3c3fc720fb0d45528657320920292d" @@ -2593,6 +2720,7 @@ dependencies = [ "checksum inotify-sys 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "e74a1aa87c59aeff6ef2cc2fa62d41bc43f54952f55652656b18a02fd5e356c0" "checksum iovec 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" "checksum itoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "b8b7a7c0c47db5545ed3fef7468ee7bb5b74691498139e4b3f6a20685dc6dd8e" +"checksum js-sys 0.3.44 (registry+https://github.com/rust-lang/crates.io-index)" = "85a7e2c92a4804dd459b86c339278d0fe87cf93757fae222c3fa3ae75458bc73" "checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" "checksum lambda_runtime 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5b8dd6cb1355fff0ff04b7c43c5d5dace2888373e13ad68e992bfe896c26ef6f" "checksum lambda_runtime_client 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b76723be69f7216b93d36f5da6f33b1d275ecb94a4355c845d5cb73df6922677" @@ -2623,6 +2751,7 @@ dependencies = [ "checksum num-rational 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)" = "ee314c74bd753fc86b4780aa9475da469155f3848473a261d2d18e35245a784e" "checksum num-traits 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "c62be47e61d1842b9170f0fdeec8eba98e60e90e5446449a0545e5152acd7096" "checksum num_cpus 1.13.0 (registry+https://github.com/rust-lang/crates.io-index)" = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" +"checksum once_cell 1.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "260e51e7efe62b592207e9e13a68e43692a7a279171d6ba57abd208bf23645ad" "checksum openssl 0.10.29 (registry+https://github.com/rust-lang/crates.io-index)" = "cee6d85f4cb4c4f59a6a85d5b68a233d280c82e29e822913b9c8b129fbf20bdd" "checksum openssl-probe 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de" "checksum openssl-sys 0.9.55 (registry+https://github.com/rust-lang/crates.io-index)" = "7717097d810a0f2e2323f9e5d11e71608355e24828410b55b9d4f18aa5f9a5d8" @@ -2637,7 +2766,7 @@ dependencies = [ "checksum prettytable-rs 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "0fd04b170004fa2daccf418a7f8253aaf033c27760b5f225889024cf66d7ac2e" "checksum proc-macro2 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "cd07deb3c6d1d9ff827999c7f9b04cdfd66b1b17ae508e14fe47b620f2282ae0" "checksum proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)" = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759" -"checksum proc-macro2 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)" = "df246d292ff63439fea9bc8c0a270bed0e390d5ebd4db4ba15aba81111b5abe3" +"checksum proc-macro2 1.0.19 (registry+https://github.com/rust-lang/crates.io-index)" = "04f5f085b5d71e2188cb8271e5da0161ad52c3f227a661a3c135fdf28e258b12" "checksum publicsuffix 1.5.4 (registry+https://github.com/rust-lang/crates.io-index)" = "3bbaa49075179162b49acac1c6aa45fb4dafb5f13cf6794276d77bc7fd95757b" "checksum quick-error 1.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" "checksum quote 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1eca14c727ad12702eb4b6bfb5a232287dcf8385cb8ca83a3eeaf6519c44c408" @@ -2668,6 +2797,7 @@ dependencies = [ "checksum regex-syntax 0.6.17 (registry+https://github.com/rust-lang/crates.io-index)" = "7fe5bd57d1d7414c6b5ed48563a2c855d995ff777729dcd91c369ec7fea395ae" "checksum remove_dir_all 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "4a83fa3702a688b9359eccba92d153ac33fd2e8462f9e0e3fdf155239ea7792e" "checksum reqwest 0.9.24 (registry+https://github.com/rust-lang/crates.io-index)" = "f88643aea3c1343c804950d7bf983bd2067f5ab59db6d613a08e05572f2714ab" +"checksum ring 0.16.15 (registry+https://github.com/rust-lang/crates.io-index)" = "952cd6b98c85bbc30efa1ba5783b8abf12fec8b3287ffa52605b9432313e34e4" "checksum rusoto_autoscaling 0.36.0 (registry+https://github.com/rust-lang/crates.io-index)" = "80a1c4f36d007eed8e96b0501593ed445c3cc6325579ff5fd4c689ef35c7eb5a" "checksum rusoto_cloudwatch 0.36.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4b11f37a855b24c1fde34ad3de8d1c2b9c5094f7799ba1681918a285cd9505a9" "checksum rusoto_core 0.36.0 (registry+https://github.com/rust-lang/crates.io-index)" = "18a699355ef3189e3bbf34b64ff5a31f06456b689b09d05cdb4a901dcf4406a8" @@ -2698,6 +2828,7 @@ dependencies = [ "checksum smallvec 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)" = "f7b0758c52e15a8b5e3691eae6cc559f08eee9406e548a4477ba4e67770a82b6" "checksum smallvec 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "05720e22615919e4734f6a99ceae50d00226c3c5aca406e102ebc33298214e0a" "checksum spectral 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ae3c15181f4b14e52eeaac3efaeec4d2764716ce9c86da0c934c3e318649c5ba" +"checksum spin 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" "checksum string 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d24114bfcceb867ca7f71a0d3fe45d45619ec47a6fbfa98cb14e14250bfa5d6d" "checksum strsim 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" "checksum structopt 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)" = "16c2cdbf9cc375f15d1b4141bc48aeef444806655cd0e904207edc8d68d86ed7" @@ -2705,7 +2836,7 @@ dependencies = [ "checksum subprocess 0.1.20 (registry+https://github.com/rust-lang/crates.io-index)" = "68713fc0f9d941642c1e020d622e6421dfe09e8891ddd4bfa2109fda9a40431d" "checksum syn 0.12.15 (registry+https://github.com/rust-lang/crates.io-index)" = "c97c05b8ebc34ddd6b967994d5c6e9852fa92f8b82b3858c39451f97346dcce5" "checksum syn 0.15.44 (registry+https://github.com/rust-lang/crates.io-index)" = "9ca4b3b69a77cbe1ffc9e198781b7acb0c7365a883670e8f1c1bc66fba79a5c5" -"checksum syn 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)" = "0df0eb663f387145cab623dea85b09c2c5b4b0aef44e945d928e682fce71bb03" +"checksum syn 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)" = "891d8d6567fe7c7f8835a3a98af4208f3846fba258c1bc3c31d6e506239f11f9" "checksum synstructure 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)" = "67656ea1dc1b41b1451851562ea232ec2e5a80242139f7e679ceccfb5d61f545" "checksum tail 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "26741f73bb5e48df8882ffb14d5f98cb32df08cfc6f029a7bb1b2ae4086b0377" "checksum tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9" @@ -2742,6 +2873,7 @@ dependencies = [ "checksum unicode-width 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "caaa9d531767d1ff2150b9332433f32a24622147e5ebb1f26409d5da67afd479" "checksum unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" "checksum unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" +"checksum untrusted 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" "checksum url 1.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "dd4e7c0d531266369519a4aa4f399d748bd37043b00bde1e4ff1f60a120b355a" "checksum url 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "829d4a8476c35c9bf0bbce5a3b23f4106f79728039b726d292bb93bc106787cb" "checksum utf8-ranges 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "b4ae116fef2b7fea257ed6440d3cfcff7f190865f170cdad00bb6465bf18ecba" @@ -2752,6 +2884,12 @@ dependencies = [ "checksum version_check 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "078775d0255232fb988e6fccf26ddc9d1ac274299aaedcedce21c6f72cc533ce" "checksum want 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b6395efa4784b027708f7451087e647ec73cc74f5d9bc2e418404248d679a230" "checksum wasi 0.9.0+wasi-snapshot-preview1 (registry+https://github.com/rust-lang/crates.io-index)" = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" +"checksum wasm-bindgen 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)" = "f0563a9a4b071746dd5aedbc3a28c6fe9be4586fb3fbadb67c400d4f53c6b16c" +"checksum wasm-bindgen-backend 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)" = "bc71e4c5efa60fb9e74160e89b93353bc24059999c0ae0fb03affc39770310b0" +"checksum wasm-bindgen-macro 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)" = "97c57cefa5fa80e2ba15641578b44d36e7a64279bc5ed43c6dbaf329457a2ed2" +"checksum wasm-bindgen-macro-support 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)" = "841a6d1c35c6f596ccea1f82504a192a60378f64b3bb0261904ad8f2f5657556" +"checksum wasm-bindgen-shared 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)" = "93b162580e34310e5931c4b792560108b10fd14d64915d7fff8ff00180e70092" +"checksum web-sys 0.3.44 (registry+https://github.com/rust-lang/crates.io-index)" = "dda38f4e5ca63eda02c059d243aa25b5f35ab98451e518c51612cd0f1bd19a47" "checksum winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" "checksum winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6" "checksum winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" diff --git a/Cargo.toml b/Cargo.toml index f8656ab..461cf53 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ members = [ "aws-watchtower", "bosun", "credentials-watchtower", + "duo", "lambda", "testing", ] diff --git a/duo/Cargo.toml b/duo/Cargo.toml new file mode 100644 index 0000000..8736e31 --- /dev/null +++ b/duo/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "duo" +version = "0.0.1" +authors = ["Lukas Pustina "] + +edition = "2018" + +[lib] +name = "duo" +path = "src/lib.rs" + +[dependencies] +base64 = "0.10" +chrono = "0.4" +failure = "0.1" +failure_derive = "0.1" +hex = "0.4" +log = "0.4" +reqwest = "0.9" +ring = "0.16" +serde = "1" +serde_derive = "1" +serde_json = "1" + +[dev-dependencies] +env_logger = "0.6" +spectral = "^0.6" +testing = { version = "0.0.1", path = "../testing" } + diff --git a/duo/src/lib.rs b/duo/src/lib.rs new file mode 100644 index 0000000..4efbe25 --- /dev/null +++ b/duo/src/lib.rs @@ -0,0 +1,180 @@ +use std::sync::Arc; +use std::time::Duration; + +use chrono::Local; +use failure::Fail; +use log::{debug, info, trace}; +use reqwest::{Method, RequestBuilder, StatusCode}; +use ring::hmac; +use std::io::Read; + +/// Result of an attempt to send meta data or a metric datum +pub type DuoResult = Result; + +/// Errors which may occur while sending either meta data or metric data. +#[derive(Debug, Fail)] +pub enum DuoError { + /// Failed to create JSON. + #[fail(display = "failed to parse JSON")] + JsonParseError, + /// Failed to create Client + #[fail(display = "failed create client because {}", _0)] + ClientError(String), + /// Failed to send to Duo + #[fail(display = "failed to send to Duo because {}", _0)] + SendError(String), + /// Failed to read from Duo + #[fail(display = "failed to process Duo response because {}", _0)] + ReceiveError(String), +} + +/// Encapsulates Duo server connection. +#[derive(Debug)] +pub struct DuoClient { + api_host_name: String, + integration_key: String, + secret_key: String, + client: Arc, +} + +impl DuoClient { + /// Creates a new DuoClient. + pub fn new, T: Into, U: Into>(api_host_name: S, integration_key: T, secret_key: U) -> DuoResult { + DuoClient::with_timeout(api_host_name, integration_key, secret_key, 5) + } + + /// Creates a new DuoClient with specific timeout + pub fn with_timeout, T: Into, U: Into>(api_host_name: S, integration_key: T, secret_key: U, timeout_sec: u64) -> DuoResult { + let client = reqwest::Client::builder() + .timeout(Duration::from_secs(timeout_sec)) + .build() + .map_err(|e| DuoError::ClientError(format!("failed to build http client because {}", e.to_string())))?; + + Ok(DuoClient { + api_host_name: api_host_name.into(), + integration_key: integration_key.into(), + secret_key: secret_key.into(), + client: Arc::new(client), + }) + } + + fn send_to_duo_api(&self, path: &str, expected: StatusCode) -> DuoResult<()> { + let uri = format!("https://{}{}", self.api_host_name, path); + + let req = self.client + .get(&uri) + //.header("Content-Type", "application/x-www-form-urlencoded"); + ; + let req = self.sign_req(req, Method::GET, path); + debug!("Request: '{:?}'", req); + + let res = req.send(); + match res { + Ok(ref response) if response.status() == expected => Ok(()), + Ok(response) => Err(DuoError::ReceiveError(format!("{}", response.status()))), + Err(err) => Err(DuoError::SendError(format!("{}", err))), + } + } + + fn sign_req(&self, req: RequestBuilder, method: Method, path: &str) -> RequestBuilder { + let now = Local::now().to_rfc2822(); + let method = method.as_str(); + let api_host_name = self.api_host_name.to_lowercase(); + let params = ""; + let canon = [now.as_str(), method, api_host_name.as_str(), path, params]; + + let basic_auth = basic_auth_for_canon(&self.integration_key, &self.secret_key, &canon); + + req + .header("Date", &now) + .header("Authorization", &basic_auth) + } +} + +fn basic_auth_for_canon(integration_key: &str, secret_key: &str, canon: &[&str]) -> String { + let canon = canon.join("\n"); + trace!("Canon: '{}'", canon); + + let s_key = hmac::Key::new(hmac::HMAC_SHA1_FOR_LEGACY_USE_ONLY, secret_key.as_bytes()); + let mut s_ctx = hmac::Context::with_key(&s_key); + s_ctx.update(canon.as_bytes()); + let sig = s_ctx.sign(); + let auth = format!("{}:{}", + integration_key, + hex::encode(sig.as_ref()) + ); + trace!("Auth: '{}'", auth); + + let basic_auth = format!("Basic {}", base64::encode(&auth)); + trace!("Basic Auth: '{}'", basic_auth); + + basic_auth +} + +pub trait Duo { + fn get_users(&self) -> DuoResult<()>; +} + +impl Duo for DuoClient { + fn get_users(&self) -> DuoResult<()> { + let res = self.send_to_duo_api("/admin/v1/users", StatusCode::OK); + + eprintln!("Res: {:?}", res); + res + } +} + +#[cfg(test)] +mod tests { + use std::env; + + use spectral::prelude::*; + + use super::*; + + + #[test] + fn basic_auth_for_canon_test() { + testing::setup(); + + let expected = "Basic RElXSjhYNkFFWU9SNU9NQzZUUTE6ZWE4MmExMzcyMGI5ZDE5MDIxNWNjODkxNzljMmNiMTcxZDg2MDdiMw=="; + + let integration_key = "DIWJ8X6AEYOR5OMC6TQ1"; + let secret_key = "Zh5eGmUq9zpfQnyUIu5OL9iWoMMv5ZNmk3zLJ4Ep"; + + let now = "Tue, 21 Aug 2012 17:29:18 -0000"; + let method = Method::POST.as_str(); + let host = "api-XXXXXXXX.duosecurity.com".to_lowercase(); + let path = "/admin/v1/users"; + let params = ""; + let canon = [now, method, host.as_str(), path, params]; + + let basic_auth = basic_auth_for_canon(integration_key, secret_key, &canon); + + assert_that(&basic_auth.as_str()).is_equal_to(&expected); + + } + + #[test] + fn get_users() { + testing::setup(); + + let api_host_name = env::var_os("DUO_API_HOST_NAME") + .expect("Environment variable 'DUO_API_HOST_NAME' is not set.") + .to_string_lossy().to_string(); + let integration_key = env::var_os("DUO_INTEGRATION_KEY") + .expect("Environment variable 'DUO_INTEGRATION_KEY' is not set.") + .to_string_lossy().to_string(); + let secret_key = env::var_os("DUO_SECRET_KEY") + .expect("Environment variable 'DUO_SECRET_KEY' is not set.") + .to_string_lossy().to_string(); + + let client = DuoClient::new(api_host_name, integration_key, secret_key) + .expect("Failed to create Duo Client"); + + let response = client.get_users(); + + assert_that(&response).is_ok(); + let response = response.expect("Request failed"); + } +} \ No newline at end of file From fa205dd9e663f77e24afeef33a9e6ae6c465b59f Mon Sep 17 00:00:00 2001 From: Lukas Pustina Date: Thu, 3 Sep 2020 11:26:46 +0200 Subject: [PATCH 6/9] Add sign example from duo --- duo/contrib/sign.py | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100755 duo/contrib/sign.py diff --git a/duo/contrib/sign.py b/duo/contrib/sign.py new file mode 100755 index 0000000..871869e --- /dev/null +++ b/duo/contrib/sign.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python2 + +import base64, hmac, hashlib, urllib + +def sign(method, host, path, params, skey, ikey): + """ + Return HTTP Basic Authentication ("Authorization" and "Date") headers. + method, host, path: strings from request + params: dict of request parameters + skey: secret key + ikey: integration key + """ + + # create canonical string + now = "Tue, 21 Aug 2012 17:29:18 -0000" + canon = [now, method.upper(), host.lower(), path] + args = [] + for key in sorted(params.keys()): + val = params[key] + if isinstance(val, unicode): + val = val.encode("utf-8") + args.append( + '%s=%s' % (urllib.quote(key, '~'), urllib.quote(val, '~'))) + canon.append('&'.join(args)) + canon = '\n'.join(canon) + print("'%s'" % canon) + + # sign canonical string + sig = hmac.new(skey, canon, hashlib.sha1) + auth = '%s:%s' % (ikey, sig.hexdigest()) + print(auth) + + # return headers + return {'Date': now, 'Authorization': 'Basic %s' % base64.b64encode(auth)} + +if __name__ == '__main__': + + params = dict() + # params["realname"] = u"First Last" + # params["username"] = u"root" + + result = sign("POST", "api-XXXXXXXX.duosecurity.com", "/admin/v1/users", params, "Zh5eGmUq9zpfQnyUIu5OL9iWoMMv5ZNmk3zLJ4Ep", "DIWJ8X6AEYOR5OMC6TQ1") + + print(result) From 9aab3e09d052b2d5361a23b6efb6b82f556ae8d3 Mon Sep 17 00:00:00 2001 From: Lukas Pustina Date: Thu, 3 Sep 2020 11:57:56 +0200 Subject: [PATCH 7/9] Add Duo User retrieval --- duo/src/lib.rs | 84 +++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 66 insertions(+), 18 deletions(-) diff --git a/duo/src/lib.rs b/duo/src/lib.rs index 4efbe25..8d7d403 100644 --- a/duo/src/lib.rs +++ b/duo/src/lib.rs @@ -1,12 +1,11 @@ -use std::sync::Arc; -use std::time::Duration; - -use chrono::Local; +use chrono::{Local, DateTime, Utc, NaiveDateTime}; use failure::Fail; use log::{debug, info, trace}; use reqwest::{Method, RequestBuilder, StatusCode}; use ring::hmac; -use std::io::Read; +use serde::{de::DeserializeOwned, Deserialize, Deserializer}; +use std::sync::Arc; +use std::time::Duration; /// Result of an attempt to send meta data or a metric datum pub type DuoResult = Result; @@ -15,8 +14,8 @@ pub type DuoResult = Result; #[derive(Debug, Fail)] pub enum DuoError { /// Failed to create JSON. - #[fail(display = "failed to parse JSON")] - JsonParseError, + #[fail(display = "failed to parse JSON because {}", _0)] + JsonParseError(String), /// Failed to create Client #[fail(display = "failed create client because {}", _0)] ClientError(String), @@ -28,6 +27,15 @@ pub enum DuoError { ReceiveError(String), } +/// Generic Duo Response +#[derive(Debug, Deserialize)] +#[serde(tag = "stat")] +#[serde(rename_all = "UPPERCASE")] +pub enum DuoResponse { + Ok { response: T }, + Fail { code: usize, message: String, message_detail: String }, +} + /// Encapsulates Duo server connection. #[derive(Debug)] pub struct DuoClient { @@ -58,19 +66,26 @@ impl DuoClient { }) } - fn send_to_duo_api(&self, path: &str, expected: StatusCode) -> DuoResult<()> { + fn send_to_duo_api(&self, path: &str, expected: StatusCode) -> DuoResult> { let uri = format!("https://{}{}", self.api_host_name, path); let req = self.client .get(&uri) //.header("Content-Type", "application/x-www-form-urlencoded"); - ; + ; let req = self.sign_req(req, Method::GET, path); debug!("Request: '{:?}'", req); let res = req.send(); match res { - Ok(ref response) if response.status() == expected => Ok(()), + Ok(mut response) if response.status() == expected => { + let text = response.text() + .map_err(|_| DuoError::ReceiveError("failed to read response body".to_string()))?; + trace!("Answer: '{}'", text); + let data = serde_json::from_str::>(&text) + .map_err(|e| DuoError::JsonParseError(e.to_string()))?; + Ok(data) + } Ok(response) => Err(DuoError::ReceiveError(format!("{}", response.status()))), Err(err) => Err(DuoError::SendError(format!("{}", err))), } @@ -111,16 +126,49 @@ fn basic_auth_for_canon(integration_key: &str, secret_key: &str, canon: &[&str]) basic_auth } +#[derive(Debug, Deserialize)] +pub struct User { + pub user_id: String, + pub username: String, + pub realname: Option, + pub email: String, + pub is_enrolled: bool, + pub status: UserStatus, + #[serde(deserialize_with = "from_unix_timestamp")] + pub last_login: Option>, +} + +fn from_unix_timestamp<'de, D>(deserializer: D) -> Result>, D::Error> +where + D: Deserializer<'de> +{ + let timestamp: Option = Option::deserialize(deserializer)?; + let utc = timestamp + .map(|x| NaiveDateTime::from_timestamp(x, 0)) + .map(|x| DateTime::from_utc(x, Utc)); + + Ok(utc) +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum UserStatus { + Active, + Bypass, + Disabled, + #[serde(rename = "locked out")] + LockedOut, + #[serde(rename = "pending deletion")] + PendingDeletion, +} + pub trait Duo { - fn get_users(&self) -> DuoResult<()>; + fn get_users(&self) -> DuoResult>>; } impl Duo for DuoClient { - fn get_users(&self) -> DuoResult<()> { - let res = self.send_to_duo_api("/admin/v1/users", StatusCode::OK); - - eprintln!("Res: {:?}", res); - res + fn get_users(&self) -> DuoResult>> { + self.send_to_duo_api("/admin/v1/users", StatusCode::OK) } } @@ -132,7 +180,6 @@ mod tests { use super::*; - #[test] fn basic_auth_for_canon_test() { testing::setup(); @@ -152,10 +199,10 @@ mod tests { let basic_auth = basic_auth_for_canon(integration_key, secret_key, &canon); assert_that(&basic_auth.as_str()).is_equal_to(&expected); - } #[test] + #[ignore] fn get_users() { testing::setup(); @@ -176,5 +223,6 @@ mod tests { assert_that(&response).is_ok(); let response = response.expect("Request failed"); + debug!("{:#?}", response) } } \ No newline at end of file From c718b6f15edb9f47fb3fa8d02434e52051d16c47 Mon Sep 17 00:00:00 2001 From: Lukas Pustina Date: Thu, 3 Sep 2020 13:30:30 +0200 Subject: [PATCH 8/9] Add Duo user disabling --- Cargo.lock | 1 + duo/Cargo.toml | 1 + duo/contrib/sign.py | 1 + duo/src/lib.rs | 121 +++++++++++++++++++++++++++++++++++++------- 4 files changed, 107 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3bd8e9c..c0e1634 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -589,6 +589,7 @@ dependencies = [ "serde_json 1.0.51 (registry+https://github.com/rust-lang/crates.io-index)", "spectral 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", "testing 0.0.1", + "url 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] diff --git a/duo/Cargo.toml b/duo/Cargo.toml index 8736e31..cd686c9 100644 --- a/duo/Cargo.toml +++ b/duo/Cargo.toml @@ -21,6 +21,7 @@ ring = "0.16" serde = "1" serde_derive = "1" serde_json = "1" +url = "2.1.1" [dev-dependencies] env_logger = "0.6" diff --git a/duo/contrib/sign.py b/duo/contrib/sign.py index 871869e..abd6bf5 100755 --- a/duo/contrib/sign.py +++ b/duo/contrib/sign.py @@ -38,6 +38,7 @@ def sign(method, host, path, params, skey, ikey): params = dict() # params["realname"] = u"First Last" # params["username"] = u"root" + params["state"] = u"disabled" result = sign("POST", "api-XXXXXXXX.duosecurity.com", "/admin/v1/users", params, "Zh5eGmUq9zpfQnyUIu5OL9iWoMMv5ZNmk3zLJ4Ep", "DIWJ8X6AEYOR5OMC6TQ1") diff --git a/duo/src/lib.rs b/duo/src/lib.rs index 8d7d403..e2f4d8a 100644 --- a/duo/src/lib.rs +++ b/duo/src/lib.rs @@ -1,11 +1,14 @@ -use chrono::{Local, DateTime, Utc, NaiveDateTime}; +use std::collections::HashMap; +use std::sync::Arc; +use std::time::Duration; + +use chrono::{DateTime, Local, NaiveDateTime, Utc}; use failure::Fail; -use log::{debug, info, trace}; +use log::{debug, trace}; use reqwest::{Method, RequestBuilder, StatusCode}; use ring::hmac; use serde::{de::DeserializeOwned, Deserialize, Deserializer}; -use std::sync::Arc; -use std::time::Duration; +use std::fmt; /// Result of an attempt to send meta data or a metric datum pub type DuoResult = Result; @@ -66,14 +69,37 @@ impl DuoClient { }) } - fn send_to_duo_api(&self, path: &str, expected: StatusCode) -> DuoResult> { + fn get_duo_api(&self, path: &str, expected: StatusCode) -> DuoResult> { + let uri = format!("https://{}{}", self.api_host_name, path); + + let req = self.client + .get(&uri); + let req = self.sign_req(req, Method::GET, path, &HashMap::new()); + debug!("Request: '{:?}'", req); + + let res = req.send(); + match res { + Ok(mut response) if response.status() == expected => { + let text = response.text() + .map_err(|_| DuoError::ReceiveError("failed to read response body".to_string()))?; + trace!("Answer: '{}'", text); + let data = serde_json::from_str::>(&text) + .map_err(|e| DuoError::JsonParseError(e.to_string()))?; + Ok(data) + } + Ok(response) => Err(DuoError::ReceiveError(format!("{}", response.status()))), + Err(err) => Err(DuoError::SendError(format!("{}", err))), + } + } + + fn post_duo_api(&self, path: &str, params: &HashMap<&str, &str>, expected: StatusCode) -> DuoResult> { let uri = format!("https://{}{}", self.api_host_name, path); let req = self.client - .get(&uri) - //.header("Content-Type", "application/x-www-form-urlencoded"); - ; - let req = self.sign_req(req, Method::GET, path); + .post(&uri) + .header("Content-Type", "application/x-www-form-urlencoded") + .query(params); + let req = self.sign_req(req, Method::POST, path, params); debug!("Request: '{:?}'", req); let res = req.send(); @@ -91,12 +117,12 @@ impl DuoClient { } } - fn sign_req(&self, req: RequestBuilder, method: Method, path: &str) -> RequestBuilder { + fn sign_req(&self, req: RequestBuilder, method: Method, path: &str, params: &HashMap<&str, &str>) -> RequestBuilder { let now = Local::now().to_rfc2822(); let method = method.as_str(); let api_host_name = self.api_host_name.to_lowercase(); - let params = ""; - let canon = [now.as_str(), method, api_host_name.as_str(), path, params]; + let params = encode_params(params); + let canon = [now.as_str(), method, api_host_name.as_str(), path, params.as_str()]; let basic_auth = basic_auth_for_canon(&self.integration_key, &self.secret_key, &canon); @@ -106,6 +132,17 @@ impl DuoClient { } } +fn encode_params(params: &HashMap<&str, &str>) -> String { + let mut sorted_keys: Vec<_> = params.keys().collect(); + sorted_keys.sort(); + let mut encoder = url::form_urlencoded::Serializer::new(String::new()); + for k in sorted_keys { + encoder.append_pair(k, params[k]); // safe + } + + encoder.finish() +} + fn basic_auth_for_canon(integration_key: &str, secret_key: &str, canon: &[&str]) -> String { let canon = canon.join("\n"); trace!("Canon: '{}'", canon); @@ -139,8 +176,8 @@ pub struct User { } fn from_unix_timestamp<'de, D>(deserializer: D) -> Result>, D::Error> -where - D: Deserializer<'de> + where + D: Deserializer<'de> { let timestamp: Option = Option::deserialize(deserializer)?; let utc = timestamp @@ -162,13 +199,34 @@ pub enum UserStatus { PendingDeletion, } +impl fmt::Display for UserStatus { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let str = match self { + UserStatus::Active => "active", + UserStatus::Bypass => "bypass", + UserStatus::Disabled => "disabled", + UserStatus::LockedOut => "locked out", + UserStatus::PendingDeletion => "pending deletion", + }; + f.write_str(str) + } +} + pub trait Duo { fn get_users(&self) -> DuoResult>>; + fn disable_user(&self, user_id: String) -> DuoResult>; } impl Duo for DuoClient { fn get_users(&self) -> DuoResult>> { - self.send_to_duo_api("/admin/v1/users", StatusCode::OK) + self.get_duo_api("/admin/v1/users", StatusCode::OK) + } + + fn disable_user(&self, user_id: String) -> DuoResult> { + let path = format!("/admin/v1/users/{}", user_id); + let disabled = UserStatus::Disabled.to_string(); + let params: HashMap<&str, &str> = [("status", disabled.as_str())].iter().cloned().collect(); + self.post_duo_api(&path, ¶ms, StatusCode::OK) } } @@ -184,7 +242,7 @@ mod tests { fn basic_auth_for_canon_test() { testing::setup(); - let expected = "Basic RElXSjhYNkFFWU9SNU9NQzZUUTE6ZWE4MmExMzcyMGI5ZDE5MDIxNWNjODkxNzljMmNiMTcxZDg2MDdiMw=="; + let expected = "Basic RElXSjhYNkFFWU9SNU9NQzZUUTE6YmU4ZTM1NWJlN2M5NjM5Y2QyYjVjYTQxMDJkZjM4MjllNmE1NzVkZg=="; let integration_key = "DIWJ8X6AEYOR5OMC6TQ1"; let secret_key = "Zh5eGmUq9zpfQnyUIu5OL9iWoMMv5ZNmk3zLJ4Ep"; @@ -193,7 +251,7 @@ mod tests { let method = Method::POST.as_str(); let host = "api-XXXXXXXX.duosecurity.com".to_lowercase(); let path = "/admin/v1/users"; - let params = ""; + let params = "state=disabled"; let canon = [now, method, host.as_str(), path, params]; let basic_auth = basic_auth_for_canon(integration_key, secret_key, &canon); @@ -225,4 +283,33 @@ mod tests { let response = response.expect("Request failed"); debug!("{:#?}", response) } + + + #[test] + #[ignore] + fn disable_user() { + testing::setup(); + + let user_id = env::var_os("DUO_USER_ID") + .expect("Environment variable 'DUO_USER_ID' is not set.") + .to_string_lossy().to_string(); + let api_host_name = env::var_os("DUO_API_HOST_NAME") + .expect("Environment variable 'DUO_API_HOST_NAME' is not set.") + .to_string_lossy().to_string(); + let integration_key = env::var_os("DUO_INTEGRATION_KEY") + .expect("Environment variable 'DUO_INTEGRATION_KEY' is not set.") + .to_string_lossy().to_string(); + let secret_key = env::var_os("DUO_SECRET_KEY") + .expect("Environment variable 'DUO_SECRET_KEY' is not set.") + .to_string_lossy().to_string(); + + let client = DuoClient::new(api_host_name, integration_key, secret_key) + .expect("Failed to create Duo Client"); + + let response = client.disable_user(user_id); + + assert_that(&response).is_ok(); + let response = response.expect("Request failed"); + debug!("{:#?}", response) + } } \ No newline at end of file From 58b16d9b3ac3c0d0c57cb3d683553074bf10529a Mon Sep 17 00:00:00 2001 From: Lukas Pustina Date: Thu, 3 Sep 2020 13:58:33 +0200 Subject: [PATCH 9/9] Add Duo to check_credentials --- Cargo.lock | 1 + credentials-watchtower/Cargo.toml | 1 + .../src/bin/check_credentials.rs | 29 +++++++--- .../src/check_credentials.rs | 55 +++++++++++++++---- duo/src/lib.rs | 9 +++ 5 files changed, 76 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c0e1634..ef68549 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -426,6 +426,7 @@ dependencies = [ "clams 0.0.13 (registry+https://github.com/rust-lang/crates.io-index)", "clams-derive 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)", "dirs 3.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "duo 0.0.1", "env_logger 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", "failure 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", "failure_derive 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/credentials-watchtower/Cargo.toml b/credentials-watchtower/Cargo.toml index 0ae515a..bbc5ebb 100644 --- a/credentials-watchtower/Cargo.toml +++ b/credentials-watchtower/Cargo.toml @@ -22,6 +22,7 @@ clams-derive = "^0.0.4" bosun = { version = "0.0.2", path = "../bosun" } chrono = { version = "0.4", features = ["serde"] } dirs = "3" +duo = { version = "0.0.1", path = "../duo" } env_logger = "0.6" failure = "0.1" failure_derive = "0.1" diff --git a/credentials-watchtower/src/bin/check_credentials.rs b/credentials-watchtower/src/bin/check_credentials.rs index 1e4e71e..9bf2bed 100644 --- a/credentials-watchtower/src/bin/check_credentials.rs +++ b/credentials-watchtower/src/bin/check_credentials.rs @@ -3,14 +3,29 @@ use prettytable::{Cell, format, Row, Table}; use rusoto_core::Region; use aws::AwsClientConfig; -use credentials_watchtower::check_credentials::{AwsCredential, check_aws_credentials, CredentialCheck}; +use credentials_watchtower::check_credentials::{Credential, check_aws_credentials, CredentialCheck, check_duo_credentials}; +use duo::DuoClient; +use std::env; fn main() { env_logger::init(); - let aws_client_config = AwsClientConfig::with_region(Region::UsEast1).expect("Failed to create AWS client config"); - let credentials = check_aws_credentials(&aws_client_config) + let api_host_name = env::var_os("DUO_API_HOST_NAME") + .expect("Environment variable 'DUO_API_HOST_NAME' is not set.") + .to_string_lossy().to_string(); + let integration_key = env::var_os("DUO_INTEGRATION_KEY") + .expect("Environment variable 'DUO_INTEGRATION_KEY' is not set.") + .to_string_lossy().to_string(); + let secret_key = env::var_os("DUO_SECRET_KEY") + .expect("Environment variable 'DUO_SECRET_KEY' is not set.") + .to_string_lossy().to_string(); + let duo_client = DuoClient::new(api_host_name, integration_key, secret_key).expect("Failed to create Duo client"); + let mut credentials = check_duo_credentials(&duo_client).expect("Failed to get Duo credentials"); + + let aws_client_config = AwsClientConfig::with_region(Region::UsEast1).expect("Failed to create AWS client config"); + let aws_redentials = check_aws_credentials(&aws_client_config) .expect("failed to load credentials"); + credentials.extend(aws_redentials); let mut table = Table::new(); table.set_format(*format::consts::FORMAT_NO_LINESEP_WITH_TITLE); @@ -26,7 +41,8 @@ fn main() { for c in &credentials { let row = match c { - CredentialCheck::Aws { credential } => aws_credential_to_row(credential), + CredentialCheck::Aws { credential } => credential_to_row("AWS", credential), + CredentialCheck::Duo { credential } => credential_to_row("Duo", credential), }; table.add_row(row); } @@ -34,9 +50,8 @@ fn main() { table.printstd(); } -fn aws_credential_to_row(credential: &AwsCredential) -> Row { - let service = "AWS"; - let user = format!("{} ({})", credential.user_name, credential.user_name); +fn credential_to_row(service: &str, credential: &Credential) -> Row { + let user = format!("{} ({})", credential.user_name, credential.id); let credential_type = format!("{:?}", credential.credential); let last_time_used = credential.last_used .map(|x| x.to_rfc3339()) diff --git a/credentials-watchtower/src/check_credentials.rs b/credentials-watchtower/src/check_credentials.rs index e72e95d..636f48e 100644 --- a/credentials-watchtower/src/check_credentials.rs +++ b/credentials-watchtower/src/check_credentials.rs @@ -1,26 +1,28 @@ -use failure::Error; +use chrono::{DateTime, Utc}; +use failure::{err_msg, Error}; use aws::AwsClientConfig; use aws::iam; -use chrono::{DateTime, Utc}; use aws::iam::AccessKeyLastUsed; +use duo::{Duo, DuoClient, DuoResponse}; #[derive(Debug)] pub enum CredentialCheck { - Aws { credential: AwsCredential }, + Aws { credential: Credential }, + Duo { credential: Credential }, } #[derive(Debug)] -pub struct AwsCredential { +pub struct Credential { pub id: String, pub user_name: String, pub credential: CredentialType, pub last_used: Option>, } -impl From for AwsCredential { +impl From for Credential { fn from(user: iam::User) -> Self { - AwsCredential { + Credential { id: user.user_id, user_name: user.user_name, credential: CredentialType::Password, @@ -29,9 +31,9 @@ impl From for AwsCredential { } } -impl From for AwsCredential { +impl From for Credential { fn from(key: AccessKeyLastUsed) -> Self { - AwsCredential { + Credential { id: key.access_key_id, user_name: key.user_name, credential: CredentialType::ApiKey, @@ -40,25 +42,36 @@ impl From for AwsCredential { } } +impl From for Credential { + fn from(user: duo::User) -> Self { + Credential { + id: user.user_id.clone(), + user_name: user.realname.clone().unwrap_or_else(|| "-".to_string()), + credential: CredentialType::TwoFA, + last_used: user.last_login.clone(), + } + } +} + #[derive(Debug)] pub enum CredentialType { Password, ApiKey, + TwoFA, } pub fn check_aws_credentials( aws_client_config: &AwsClientConfig, ) -> Result, Error> { - let users = iam::list_users(&aws_client_config)?; let mut credentials: Vec = Vec::new(); - let user_credentials: Vec = users.clone().into_iter() + let user_credentials: Vec = users.clone().into_iter() .map(Into::into) .collect(); let _ = user_credentials.into_iter() - .map(|x| CredentialCheck::Aws { credential: x}) + .map(|x| CredentialCheck::Aws { credential: x }) .map(|x| credentials.push(x)).collect::>(); let access_keys: Vec<_> = users.into_iter() @@ -74,8 +87,26 @@ pub fn check_aws_credentials( .collect(); let _ = access_keys.into_iter() .map(Into::into) - .map(|x| CredentialCheck::Aws {credential: x}) + .map(|x| CredentialCheck::Aws { credential: x }) .map(|x| credentials.push(x)).collect::>(); Ok(credentials) } + +pub fn check_duo_credentials( + duo_client: &DuoClient, +) -> Result, Error> { + let response = duo_client.get_users()?; + match response { + DuoResponse::Ok { response: users } => Ok(users + .into_iter() + .map(Into::into) + .map(|x| CredentialCheck::Duo { credential: x }) + .collect() + ), + DuoResponse::Fail { code, message, message_detail } => { + let msg = format!("failed to get Duo users (code: {}) because {}, {}", code, message, message_detail); + Err(err_msg(msg)) + } + } +} diff --git a/duo/src/lib.rs b/duo/src/lib.rs index e2f4d8a..f1d0961 100644 --- a/duo/src/lib.rs +++ b/duo/src/lib.rs @@ -39,6 +39,15 @@ pub enum DuoResponse { Fail { code: usize, message: String, message_detail: String }, } +impl DuoResponse { + pub fn ok(&self) -> Option<&T> { + match self { + DuoResponse::Ok { response: ref data } => Some(data), + DuoResponse::Fail { .. } => None, + } + } +} + /// Encapsulates Duo server connection. #[derive(Debug)] pub struct DuoClient {