use std::error::Error as StdError;
use super::{
config::{AwsConfig, AwsType, MatchValidationType},
match_validator::MatchValidator,
validator_utils::generate_aws_headers_and_body,
};
use crate::scanner::RootCompiledRule;
use crate::{HttpErrorInfo, ValidationError, match_validation::match_validator::RAYON_THREAD_POOL};
use crate::{MatchStatus, RuleMatch};
use ahash::AHashMap;
use lazy_static::lazy_static;
use reqwest::Client;
lazy_static! {
static ref AWS_CLIENT: Client = Client::new();
static ref AWS_BLOCKING_CLIENT: reqwest::blocking::Client = reqwest::blocking::Client::new();
static ref AWS_SECRET_REGEX: regex::Regex =
regex::Regex::new(r"([A-Za-z0-9\/+]{40})\b").unwrap();
}
pub struct AwsValidator {
pub config: AwsConfig,
}
impl AwsValidator {
pub fn new(config: AwsConfig) -> Self {
AwsValidator { config }
}
fn get_match_status_per_pairs_of_matches_idx(
&self,
matches: &[RuleMatch],
scanner_rules: &[RootCompiledRule],
) -> AHashMap<(usize, usize), MatchStatus> {
let mut aws_id_matches_idx = vec![];
let mut aws_secret_matches_idx = vec![];
for (idx, m) in matches.iter().enumerate() {
let rule = &scanner_rules[m.rule_index];
if let Some(MatchValidationType::Aws(aws_type)) = &rule.match_validation_type {
match aws_type {
AwsType::AwsId => {
aws_id_matches_idx.push(idx);
}
AwsType::AwsSecret(_) => {
aws_secret_matches_idx.push(idx);
}
AwsType::AwsSession => {
}
}
}
}
let mut match_status_per_pairs_of_matches_idx: AHashMap<(usize, usize), MatchStatus> =
AHashMap::new();
for aws_id_match_idx in &aws_id_matches_idx {
for aws_secret_match_idx in &aws_secret_matches_idx {
match_status_per_pairs_of_matches_idx.insert(
(*aws_id_match_idx, *aws_secret_match_idx),
MatchStatus::NotChecked,
);
}
}
match_status_per_pairs_of_matches_idx
}
}
fn extract_aws_secret_from_match(match_value: &str) -> String {
let caps = AWS_SECRET_REGEX.captures(match_value);
if let Some(caps) = caps {
return caps[1].to_string();
}
"".to_string()
}
fn merge_returned_match_status_with_better_status(
matches: &mut [RuleMatch],
match_status_per_pairs_of_matches_idx: &AHashMap<(usize, usize), MatchStatus>,
) {
for ((id_index, secret_index), match_status) in match_status_per_pairs_of_matches_idx {
{
matches[*id_index].match_status.merge(match_status.clone());
matches[*secret_index]
.match_status
.merge(match_status.clone());
}
}
}
fn handle_reqwest_response(match_status: &mut MatchStatus, val: &reqwest::blocking::Response) {
if val.status().is_success() {
*match_status = MatchStatus::Valid;
return;
}
if val.status().is_client_error() {
*match_status = MatchStatus::Invalid;
return;
}
if val.status().is_server_error() {
*match_status =
MatchStatus::ValidationError(vec![ValidationError::HttpError(HttpErrorInfo {
status_code: val.status().as_u16(),
message: "Unexpected HTTP status code".to_string(),
})]);
}
}
impl MatchValidator for AwsValidator {
fn validate(&self, matches: &mut Vec<RuleMatch>, scanner_rules: &[RootCompiledRule]) {
let mut match_status_per_pairs_of_matches_idx: AHashMap<(usize, usize), MatchStatus> =
self.get_match_status_per_pairs_of_matches_idx(matches, scanner_rules);
RAYON_THREAD_POOL.install(|| {
use rayon::prelude::*;
match_status_per_pairs_of_matches_idx
.par_iter_mut()
.for_each(|((id_index, secret_index), match_status)| {
let match_id = &matches[*id_index].match_value;
let match_secret = &matches[*secret_index].match_value;
if match_secret.is_none() {
*match_status = MatchStatus::MissingDependentMatch;
return;
}
if match_id.is_none() {
*match_status = MatchStatus::MissingDependentMatch;
return;
}
let match_secret =
extract_aws_secret_from_match(match_secret.as_ref().unwrap());
let match_id = match_id.as_ref().unwrap();
let mut datetime = chrono::Utc::now();
if let Some(forced_date) = self.config.forced_datetime_utc {
datetime = forced_date;
}
let (body, headers) = generate_aws_headers_and_body(
&datetime,
&self.config.aws_sts_endpoint,
match_id,
&match_secret,
);
let res = AWS_BLOCKING_CLIENT
.post(self.config.aws_sts_endpoint.as_str())
.headers(headers)
.body(body)
.timeout(self.config.timeout)
.send();
match res {
Ok(val) => handle_reqwest_response(match_status, &val),
Err(err) => {
let mut msg = format!("Error making HTTP request: {err}");
let mut status_code = 0;
if err.is_timeout() {
msg.push_str(": timeout");
} else if err.is_connect() {
msg.push_str(": connect error");
}
if let Some(status) = err.status() {
status_code = status.as_u16();
}
if let Some(source) = StdError::source(&err) {
msg.push_str(format!(": {}", source).as_str());
}
*match_status =
MatchStatus::ValidationError(vec![ValidationError::HttpError(
HttpErrorInfo {
status_code,
message: msg,
},
)]);
}
};
});
});
merge_returned_match_status_with_better_status(
matches,
&match_status_per_pairs_of_matches_idx,
);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_aws_secret_from_match() {
assert_eq!(
extract_aws_secret_from_match(
"aws_secret_access_key=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
),
"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
);
assert_eq!(
extract_aws_secret_from_match(
"aws_secret_access_key wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
),
"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
);
assert_eq!(
extract_aws_secret_from_match("wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"),
"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
);
}
}