use super::ProbeContext;
use std::collections::HashMap;
pub fn extract_cloud_features(ctx: &ProbeContext, features: &mut HashMap<String, f64>) {
let body_lower = ctx.response.body.to_lowercase();
let url_lower = ctx.request_url.to_lowercase();
if url_lower.contains(".s3.amazonaws.com")
|| url_lower.contains("s3.")
|| body_lower.contains("<listbucketresult")
{
if ctx.response.status == 200 {
features.insert("cloud:s3_bucket_public_read".into(), 1.0);
}
}
if url_lower.contains(".s3.amazonaws.com") && ctx.request_method == "PUT" {
if ctx.response.status < 300 {
features.insert("cloud:s3_bucket_public_write".into(), 1.0);
}
}
if body_lower.contains("<listbucketresult") {
features.insert("cloud:s3_bucket_listing".into(), 1.0);
}
if url_lower.contains("storage.googleapis.com") && ctx.response.status == 200 {
features.insert("cloud:gcs_bucket_public".into(), 1.0);
}
if (url_lower.contains(".blob.core.windows.net")
|| url_lower.contains("azure"))
&& !url_lower.contains("sig=")
&& ctx.response.status == 200
{
features.insert("cloud:azure_blob_public".into(), 1.0);
}
if (body_lower.contains("169.254.169.254") || url_lower.contains("169.254.169.254"))
&& ctx.response.status == 200
{
features.insert("cloud:imds_v1_accessible".into(), 1.0);
}
if (body_lower.contains("accesskeyid") && body_lower.contains("secretaccesskey"))
|| has_aws_key_pattern(&ctx.response.body)
{
features.insert("cloud:iam_role_credentials_leaked".into(), 1.0);
}
if body_lower.contains("\"type\": \"service_account\"")
|| body_lower.contains("\"type\":\"service_account\"")
{
if body_lower.contains("private_key") {
features.insert("cloud:service_account_key_leaked".into(), 1.0);
}
}
if body_lower.contains("\"effect\":\"allow\"")
|| body_lower.contains("\"effect\": \"allow\"")
{
if body_lower.contains("\"action\":\"*\"") || body_lower.contains("\"action\": \"*\"") {
if body_lower.contains("\"resource\":\"*\"")
|| body_lower.contains("\"resource\": \"*\"")
{
features.insert("cloud:iam_policy_misconfigured".into(), 1.0);
}
}
}
if body_lower.contains("aws_lambda_function_name")
|| body_lower.contains("aws_lambda_log_group")
|| body_lower.contains("_handler")
&& body_lower.contains("aws_region")
{
features.insert("cloud:lambda_env_leaked".into(), 1.0);
}
if body_lower.contains("arn:aws:lambda") && body_lower.contains(":layer:") {
features.insert("cloud:lambda_layer_accessible".into(), 1.0);
}
if body_lower.contains("169.254.170.2") || url_lower.contains("169.254.170.2") {
if ctx.response.status == 200 {
features.insert("cloud:ecs_task_metadata".into(), 1.0);
}
}
if body_lower.contains("\"kind\":\"namespacelist\"")
|| body_lower.contains("\"kind\": \"namespacelist\"")
|| (body_lower.contains("\"apiversion\":\"v1\"")
&& body_lower.contains("\"kind\""))
{
features.insert("cloud:k8s_api_unauthenticated".into(), 1.0);
}
if body_lower.contains("kubernetes dashboard")
|| body_lower.contains("k8s-dashboard")
{
features.insert("cloud:k8s_dashboard_exposed".into(), 1.0);
}
if url_lower.contains("/v2/_catalog") && ctx.response.status == 200 {
if body_lower.contains("\"repositories\"") {
features.insert("cloud:docker_registry_public".into(), 1.0);
}
}
if body_lower.contains("cognito") && body_lower.contains("userpool") {
features.insert("cloud:cognito_pool_misconfigured".into(), 1.0);
}
if url_lower.contains("firebaseio.com") && ctx.response.status == 200 {
if body_lower.starts_with('{') || body_lower.starts_with('[') {
features.insert("cloud:firebase_db_public".into(), 1.0);
}
}
if body_lower.contains("\"cluster_name\"")
|| body_lower.contains("/_cat/indices")
|| (body_lower.contains("\"tagline\"")
&& body_lower.contains("you know, for search"))
{
features.insert("cloud:elasticsearch_unauthenticated".into(), 1.0);
}
if body_lower.contains("github_token") || body_lower.contains("actions_runtime_token") {
features.insert("cloud:github_actions_secret_leak".into(), 1.0);
}
if body_lower.contains("\"terraform_version\"") && body_lower.contains("\"serial\"") {
features.insert("cloud:terraform_state_public".into(), 1.0);
}
if body_lower.contains("<title>dashboard [jenkins]</title>")
|| ctx.response.headers.contains_key("x-jenkins")
{
features.insert("cloud:jenkins_unauthenticated".into(), 1.0);
}
if body_lower.contains("ci_job_token") || body_lower.contains("gitlab-ci-token") {
features.insert("cloud:gitlab_ci_token_leak".into(), 1.0);
}
if body_lower.contains("<receivemessageresponse")
|| body_lower.contains("sqs.amazonaws.com")
{
if ctx.response.status == 200 {
features.insert("cloud:aws_sqs_public".into(), 1.0);
}
}
if body_lower.contains("<listsubscriptionsresponse")
|| body_lower.contains("sns.amazonaws.com")
{
if ctx.response.status == 200 {
features.insert("cloud:aws_sns_public".into(), 1.0);
}
}
}
fn has_aws_key_pattern(body: &str) -> bool {
let bytes = body.as_bytes();
for i in 0..bytes.len().saturating_sub(19) {
if &bytes[i..i + 4] == b"AKIA" {
let rest = &bytes[i + 4..i + 20];
if rest
.iter()
.all(|b| b.is_ascii_uppercase() || b.is_ascii_digit())
{
return true;
}
}
}
false
}
#[cfg(test)]
mod tests {
use super::super::tests::*;
use super::*;
#[test]
fn test_s3_bucket_listing() {
let response = make_response(
"<?xml version=\"1.0\"?><ListBucketResult><Name>mybucket</Name></ListBucketResult>",
200,
);
let mut ctx = make_ctx("ssrf", "http://s3.amazonaws.com/mybucket", response);
ctx.request_url = "https://mybucket.s3.amazonaws.com/".to_string();
let mut features = HashMap::new();
extract_cloud_features(&ctx, &mut features);
assert!(features.contains_key("cloud:s3_bucket_listing"));
assert!(features.contains_key("cloud:s3_bucket_public_read"));
}
#[test]
fn test_imds_accessible() {
let response = make_response("ami-id\ninstance-id\ninstance-type", 200);
let mut ctx = make_ctx("ssrf", "http://169.254.169.254/latest/meta-data", response);
ctx.request_url = "http://169.254.169.254/latest/meta-data".to_string();
let mut features = HashMap::new();
extract_cloud_features(&ctx, &mut features);
assert!(features.contains_key("cloud:imds_v1_accessible"));
}
#[test]
fn test_iam_credentials_leaked() {
let response = make_response(
"{\"AccessKeyId\": \"AKIAIOSFODNN7EXAMPLE\", \"SecretAccessKey\": \"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY\"}",
200,
);
let ctx = make_ctx("ssrf", "http://169.254.169.254", response);
let mut features = HashMap::new();
extract_cloud_features(&ctx, &mut features);
assert!(features.contains_key("cloud:iam_role_credentials_leaked"));
}
#[test]
fn test_aws_key_pattern() {
assert!(has_aws_key_pattern("key=AKIAIOSFODNN7EXAMPLE"));
assert!(!has_aws_key_pattern("key=NOTANACCESSKEY123"));
assert!(!has_aws_key_pattern("key=AKIAtooShort"));
}
#[test]
fn test_terraform_state() {
let response = make_response(
"{\"version\": 4, \"terraform_version\": \"1.5.0\", \"serial\": 12}",
200,
);
let ctx = make_ctx("traversal", "/terraform.tfstate", response);
let mut features = HashMap::new();
extract_cloud_features(&ctx, &mut features);
assert!(features.contains_key("cloud:terraform_state_public"));
}
#[test]
fn test_jenkins_unauthenticated() {
let mut response = make_response(
"<html><head><title>Dashboard [Jenkins]</title></head></html>",
200,
);
response
.headers
.insert("x-jenkins".to_string(), "2.414.1".to_string());
let ctx = make_ctx("traversal", "/", response);
let mut features = HashMap::new();
extract_cloud_features(&ctx, &mut features);
assert!(features.contains_key("cloud:jenkins_unauthenticated"));
}
#[test]
fn test_firebase_public() {
let response = make_response("{\"users\": [{\"name\": \"admin\"}]}", 200);
let mut ctx = make_ctx("ssrf", "https://myapp.firebaseio.com/.json", response);
ctx.request_url = "https://myapp.firebaseio.com/.json".to_string();
let mut features = HashMap::new();
extract_cloud_features(&ctx, &mut features);
assert!(features.contains_key("cloud:firebase_db_public"));
}
#[test]
fn test_elasticsearch_unauthenticated() {
let response = make_response(
"{\"name\": \"node-1\", \"cluster_name\": \"production\", \"tagline\": \"You Know, for Search\"}",
200,
);
let ctx = make_ctx("ssrf", "http://10.0.0.1:9200/", response);
let mut features = HashMap::new();
extract_cloud_features(&ctx, &mut features);
assert!(features.contains_key("cloud:elasticsearch_unauthenticated"));
}
}