use std::collections::BTreeMap;
use std::future::Future;
use std::io::Write;
use std::pin::Pin;
use anyhow::Result;
use schemars::JsonSchema;
use serde::Serialize;
use crate::cli::commands::access_map::{AccessMapArgs, AccessMapOutputFormat, AccessMapProvider};
mod airtable;
mod algolia;
mod alibaba;
mod anthropic;
mod artifactory;
mod asana;
mod auth0;
mod aws;
mod azure;
mod azure_devops;
mod bitbucket;
mod buildkite;
mod circleci;
mod digitalocean;
mod fastly;
mod gcp;
mod gitea;
mod github;
mod gitlab;
mod harness;
mod hubspot;
mod huggingface;
mod ibm_cloud;
mod jira;
mod microsoft_teams;
mod monday;
pub(crate) mod mongodb;
pub(crate) mod mysql;
mod openai;
mod paypal;
mod pinecone;
mod plaid;
pub(crate) mod postgres;
pub(crate) mod report;
mod salesforce;
mod sendgrid;
mod sendinblue;
mod shopify;
mod slack;
mod square;
mod stripe;
mod terraform;
mod weightsandbiases;
mod xray;
mod zendesk;
pub trait TokenAccessMapper: Send + Sync {
fn cloud_name(&self) -> &'static str;
fn map_access_from_token(
&self,
token: &str,
) -> impl std::future::Future<Output = Result<AccessMapResult>> + Send;
}
pub async fn run(args: AccessMapArgs) -> Result<()> {
let result = dispatch_cli_request(&args).await?;
let mut writer = args.output_args.get_writer()?;
match args.output_args.format {
AccessMapOutputFormat::Json => {
serde_json::to_writer_pretty(&mut writer, &result)?;
writeln!(writer)?;
}
AccessMapOutputFormat::Html => {
let html = report::render_html_report_multi(&[result])?;
writer.write_all(html.as_bytes())?;
}
}
Ok(())
}
type AccessMapResultFuture<'a> = Pin<Box<dyn Future<Output = Result<AccessMapResult>> + Send + 'a>>;
type MappedRequestFuture = Pin<Box<dyn Future<Output = (AccessMapAttempt, String)> + Send>>;
fn dispatch_cli_request(args: &AccessMapArgs) -> AccessMapResultFuture<'_> {
match &args.provider {
AccessMapProvider::Gcp => Box::pin(gcp::map_access(args.credential_path.as_deref())),
AccessMapProvider::Aws => Box::pin(aws::map_access(args)),
AccessMapProvider::Azure => Box::pin(azure::map_access(args)),
AccessMapProvider::Github => Box::pin(github::map_access(args)),
AccessMapProvider::Gitlab => Box::pin(gitlab::map_access(args)),
AccessMapProvider::Slack => Box::pin(slack::map_access(args)),
AccessMapProvider::Postgres => Box::pin(postgres::map_access(args)),
AccessMapProvider::Mongodb => Box::pin(mongodb::map_access(args)),
AccessMapProvider::Huggingface => Box::pin(huggingface::map_access(args)),
AccessMapProvider::Gitea => Box::pin(gitea::map_access(args)),
AccessMapProvider::Bitbucket => Box::pin(bitbucket::map_access(args)),
AccessMapProvider::Buildkite => Box::pin(buildkite::map_access(args)),
AccessMapProvider::Harness => Box::pin(harness::map_access(args)),
AccessMapProvider::Openai => Box::pin(openai::map_access(args)),
AccessMapProvider::Anthropic => Box::pin(anthropic::map_access(args)),
AccessMapProvider::Salesforce => Box::pin(salesforce::map_access(args)),
AccessMapProvider::Weightsandbiases => Box::pin(weightsandbiases::map_access(args)),
AccessMapProvider::Microsoftteams => Box::pin(microsoft_teams::map_access(args)),
AccessMapProvider::Airtable => Box::pin(airtable::map_access(args)),
AccessMapProvider::Alibaba => Box::pin(alibaba::map_access(args)),
AccessMapProvider::Circleci => Box::pin(circleci::map_access(args)),
AccessMapProvider::Digitalocean => Box::pin(digitalocean::map_access(args)),
AccessMapProvider::Fastly => Box::pin(fastly::map_access(args)),
AccessMapProvider::Hubspot => Box::pin(hubspot::map_access(args)),
AccessMapProvider::Ibmcloud => Box::pin(ibm_cloud::map_access(args)),
AccessMapProvider::Sendgrid => Box::pin(sendgrid::map_access(args)),
AccessMapProvider::Sendinblue => Box::pin(sendinblue::map_access(args)),
AccessMapProvider::Stripe => Box::pin(stripe::map_access(args)),
AccessMapProvider::Terraform => Box::pin(terraform::map_access(args)),
AccessMapProvider::Square => Box::pin(square::map_access(args)),
AccessMapProvider::Jira => Box::pin(jira::map_access(args)),
AccessMapProvider::Mysql => Box::pin(mysql::map_access(args)),
AccessMapProvider::Algolia => Box::pin(algolia::map_access(args)),
AccessMapProvider::Auth0 => Box::pin(auth0::map_access(args)),
AccessMapProvider::Paypal => Box::pin(paypal::map_access(args)),
AccessMapProvider::Plaid => Box::pin(plaid::map_access(args)),
AccessMapProvider::Shopify => Box::pin(shopify::map_access(args)),
AccessMapProvider::Zendesk => Box::pin(zendesk::map_access(args)),
AccessMapProvider::Artifactory => Box::pin(artifactory::map_access(args)),
AccessMapProvider::Xray => Box::pin(xray::map_access(args)),
AccessMapProvider::Monday => Box::pin(monday::map_access(args)),
AccessMapProvider::Asana => Box::pin(asana::map_access(args)),
AccessMapProvider::Pinecone => Box::pin(pinecone::map_access(args)),
}
}
#[derive(Clone, Debug)]
pub enum AccessMapRequest {
Aws {
access_key: String,
secret_key: String,
session_token: Option<String>,
fingerprint: String,
},
Gcp { credential_json: String, fingerprint: String },
GcpApiKey { api_key: String, fingerprint: String },
Azure { credential_json: String, containers: Option<Vec<String>>, fingerprint: String },
AzureDevops { token: String, organization: String, fingerprint: String },
Github { token: String, fingerprint: String },
Gitlab { token: String, fingerprint: String },
Slack { token: String, fingerprint: String },
Postgres { uri: String, fingerprint: String },
MongoDB { uri: String, fingerprint: String },
HuggingFace { token: String, fingerprint: String },
Gitea { token: String, fingerprint: String },
Bitbucket { token: String, fingerprint: String },
Buildkite { token: String, fingerprint: String },
Harness { token: String, fingerprint: String },
OpenAI { token: String, fingerprint: String },
Anthropic { token: String, fingerprint: String },
Salesforce { token: String, instance: String, fingerprint: String },
WeightsAndBiases { token: String, fingerprint: String },
MicrosoftTeams { webhook_url: String, fingerprint: String },
Airtable { token: String, fingerprint: String },
Alibaba {
access_key: String,
secret_key: String,
session_token: Option<String>,
fingerprint: String,
},
CircleCI { token: String, fingerprint: String },
DigitalOcean { token: String, fingerprint: String },
Fastly { token: String, fingerprint: String },
HubSpot { token: String, fingerprint: String },
IbmCloud { token: String, fingerprint: String },
SendGrid { token: String, fingerprint: String },
Sendinblue { token: String, fingerprint: String },
Stripe { token: String, fingerprint: String },
Terraform { token: String, fingerprint: String },
Square { token: String, fingerprint: String },
Jira { token: String, base_url: String, fingerprint: String },
MySQL { uri: String, fingerprint: String },
Algolia { app_id: String, api_key: String, fingerprint: String },
Auth0 { client_id: String, client_secret: String, domain: String, fingerprint: String },
PayPal { client_id: String, client_secret: String, fingerprint: String },
Plaid { client_id: String, secret: String, fingerprint: String },
Shopify { token: String, subdomain: String, fingerprint: String },
Zendesk { token: String, subdomain: String, fingerprint: String },
Artifactory { token: String, base_url: Option<String>, fingerprint: String },
Xray { token: String, base_url: Option<String>, fingerprint: String },
Monday { token: String, fingerprint: String },
Asana { token: String, fingerprint: String },
Pinecone { token: String, fingerprint: String },
}
impl AccessMapRequest {
pub(crate) fn finding_fingerprint(&self) -> &str {
match self {
Self::Aws { fingerprint, .. }
| Self::Gcp { fingerprint, .. }
| Self::GcpApiKey { fingerprint, .. }
| Self::Azure { fingerprint, .. }
| Self::AzureDevops { fingerprint, .. }
| Self::Github { fingerprint, .. }
| Self::Gitlab { fingerprint, .. }
| Self::Slack { fingerprint, .. }
| Self::Postgres { fingerprint, .. }
| Self::MongoDB { fingerprint, .. }
| Self::HuggingFace { fingerprint, .. }
| Self::Gitea { fingerprint, .. }
| Self::Bitbucket { fingerprint, .. }
| Self::Buildkite { fingerprint, .. }
| Self::Harness { fingerprint, .. }
| Self::OpenAI { fingerprint, .. }
| Self::Anthropic { fingerprint, .. }
| Self::Salesforce { fingerprint, .. }
| Self::WeightsAndBiases { fingerprint, .. }
| Self::MicrosoftTeams { fingerprint, .. }
| Self::Airtable { fingerprint, .. }
| Self::Alibaba { fingerprint, .. }
| Self::CircleCI { fingerprint, .. }
| Self::DigitalOcean { fingerprint, .. }
| Self::Fastly { fingerprint, .. }
| Self::HubSpot { fingerprint, .. }
| Self::IbmCloud { fingerprint, .. }
| Self::SendGrid { fingerprint, .. }
| Self::Sendinblue { fingerprint, .. }
| Self::Stripe { fingerprint, .. }
| Self::Terraform { fingerprint, .. }
| Self::Square { fingerprint, .. }
| Self::Jira { fingerprint, .. }
| Self::MySQL { fingerprint, .. }
| Self::Algolia { fingerprint, .. }
| Self::Auth0 { fingerprint, .. }
| Self::PayPal { fingerprint, .. }
| Self::Plaid { fingerprint, .. }
| Self::Shopify { fingerprint, .. }
| Self::Zendesk { fingerprint, .. }
| Self::Artifactory { fingerprint, .. }
| Self::Xray { fingerprint, .. }
| Self::Monday { fingerprint, .. }
| Self::Asana { fingerprint, .. }
| Self::Pinecone { fingerprint, .. } => fingerprint,
}
}
}
#[derive(Clone, Debug)]
pub(crate) struct CollectedAccessMapRequest {
pub request: AccessMapRequest,
pub finding_fingerprints: Vec<String>,
}
#[derive(Debug, Serialize, Clone)]
pub struct AccessMapResult {
pub cloud: String,
pub fingerprint: Option<String>,
pub identity: AccessSummary,
pub roles: Vec<RoleBinding>,
pub permissions: PermissionSummary,
pub resources: Vec<ResourceExposure>,
pub severity: Severity,
pub recommendations: Vec<String>,
pub risk_notes: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub token_details: Option<AccessTokenDetails>,
#[serde(skip_serializing_if = "Option::is_none")]
pub provider_metadata: Option<ProviderMetadata>,
}
#[derive(Debug)]
pub(crate) struct AccessMapAttempt {
pub result: AccessMapResult,
pub succeeded: bool,
}
#[derive(Debug, Clone)]
pub(crate) struct ScanAccessMapResult {
pub result: AccessMapResult,
pub finding_fingerprints: Vec<String>,
pub mapping_succeeded: bool,
}
#[derive(Debug, Serialize, Clone)]
pub struct AccessSummary {
pub id: String,
pub access_type: String,
pub project: Option<String>,
pub tenant: Option<String>,
pub account_id: Option<String>,
}
#[derive(Debug, Serialize, Clone, JsonSchema)]
pub struct RoleBinding {
pub name: String,
pub source: String,
pub permissions: Vec<String>,
}
#[derive(Debug, Serialize, Default, Clone, JsonSchema)]
pub struct PermissionSummary {
pub admin: Vec<String>,
pub privilege_escalation: Vec<String>,
pub risky: Vec<String>,
pub read_only: Vec<String>,
}
impl PermissionSummary {
pub fn is_empty(&self) -> bool {
self.admin.is_empty()
&& self.privilege_escalation.is_empty()
&& self.risky.is_empty()
&& self.read_only.is_empty()
}
pub fn total(&self) -> usize {
self.admin.len() + self.privilege_escalation.len() + self.risky.len() + self.read_only.len()
}
}
#[derive(Debug, Serialize, Clone)]
pub struct ResourceExposure {
pub resource_type: String,
pub name: String,
pub permissions: Vec<String>,
pub risk: String,
pub reason: String,
}
#[derive(Debug, Serialize, Clone, Copy)]
pub enum Severity {
Low,
Medium,
High,
Critical,
}
#[derive(Debug, Serialize, Clone, Default, JsonSchema)]
pub struct AccessTokenDetails {
pub name: Option<String>,
pub username: Option<String>,
pub account_type: Option<String>,
pub company: Option<String>,
pub location: Option<String>,
pub email: Option<String>,
pub url: Option<String>,
pub token_type: Option<String>,
pub created_at: Option<String>,
pub last_used_at: Option<String>,
pub expires_at: Option<String>,
pub user_id: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub scopes: Vec<String>,
}
#[derive(Debug, Serialize, Clone, Default, JsonSchema)]
pub struct ProviderMetadata {
pub version: Option<String>,
pub enterprise: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub authorization_evidence: Option<AuthorizationEvidence>,
}
#[derive(Debug, Serialize, Clone, Default, JsonSchema)]
pub struct AuthorizationEvidence {
#[serde(skip_serializing_if = "Option::is_none")]
pub principal: Option<PrincipalEvidence>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub policies: Vec<PolicyEvidence>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub paths: Vec<AuthorizationPath>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub role_impacts: Vec<RoleImpact>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub hierarchy: Vec<HierarchyScope>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub probes: Vec<AccessProbeEvidence>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub limitations: Vec<String>,
}
#[derive(Debug, Serialize, Clone, Default, JsonSchema)]
pub struct AccessProbeEvidence {
pub service: String,
pub method: String,
pub status: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub http_status: Option<u16>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
}
#[derive(Debug, Serialize, Clone, Default, JsonSchema)]
pub struct RoleImpact {
#[serde(skip_serializing_if = "Option::is_none")]
pub target: Option<String>,
pub role: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
pub status: String,
pub hop_count: usize,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub permissions: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub grants: Vec<AuthorizationGrant>,
}
#[derive(Debug, Serialize, Clone, Default, JsonSchema)]
pub struct AuthorizationGrant {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub permissions: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub excluded_permissions: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub resources: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub excluded_resources: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub condition_keys: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub evidence: Vec<String>,
}
#[derive(Debug, Serialize, Clone, Default, JsonSchema)]
pub struct PrincipalEvidence {
pub id: String,
pub kind: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub canonical_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub groups: Vec<String>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub tags: BTreeMap<String, String>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub attributes: BTreeMap<String, String>,
}
#[derive(Debug, Serialize, Clone, Default, JsonSchema)]
pub struct PolicyEvidence {
pub id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
pub kind: String,
pub attached_to: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub attached_via: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub scope: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub version: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub statements: Vec<AuthorizationStatement>,
}
#[derive(Debug, Serialize, Clone, Default, JsonSchema)]
pub struct AuthorizationStatement {
pub id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub sid: Option<String>,
pub effect: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub actions: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub not_actions: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub resources: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub not_resources: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub principals: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub not_principals: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub condition_keys: Vec<String>,
}
#[derive(Debug, Serialize, Clone, Default, JsonSchema)]
pub struct AuthorizationPath {
#[serde(skip_serializing_if = "Option::is_none")]
pub direction: Option<String>,
pub status: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub hops: Vec<AuthorizationHop>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub evidence: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub conditions: Vec<String>,
}
#[derive(Debug, Serialize, Clone, Default, JsonSchema)]
pub struct AuthorizationHop {
pub from: String,
pub to: String,
pub relationship: String,
}
#[derive(Debug, Serialize, Clone, Default, JsonSchema)]
pub struct HierarchyScope {
pub kind: String,
pub id: String,
}
pub async fn map_requests(requests: Vec<AccessMapRequest>) -> Vec<AccessMapResult> {
map_request_attempts(requests).await.into_iter().map(|attempt| attempt.result).collect()
}
pub(crate) async fn map_request_attempts(requests: Vec<AccessMapRequest>) -> Vec<AccessMapAttempt> {
let mut results = Vec::new();
for request in requests {
let (mut attempt, fp) = dispatch_access_map_request(request).await;
attempt.result.fingerprint = Some(fp);
results.push(attempt);
}
results
}
pub(crate) async fn map_collected_requests(
requests: Vec<CollectedAccessMapRequest>,
) -> Vec<ScanAccessMapResult> {
let mut results = Vec::with_capacity(requests.len());
for collected in requests {
let (mut attempt, primary_fingerprint) =
dispatch_access_map_request(collected.request).await;
let mut finding_fingerprints = collected.finding_fingerprints;
finding_fingerprints.push(primary_fingerprint);
finding_fingerprints.sort_unstable();
finding_fingerprints.dedup();
attempt.result.fingerprint = finding_fingerprints.first().cloned();
results.push(ScanAccessMapResult {
result: attempt.result,
finding_fingerprints,
mapping_succeeded: attempt.succeeded,
});
}
results
}
fn finish_mapping(
result: Result<AccessMapResult>,
cloud: &str,
identity_label: &str,
) -> AccessMapAttempt {
match result {
Ok(result) => AccessMapAttempt { result, succeeded: true },
Err(err) => AccessMapAttempt {
result: build_failed_result(cloud, identity_label, err),
succeeded: false,
},
}
}
fn dispatch_access_map_request(request: AccessMapRequest) -> MappedRequestFuture {
match request {
AccessMapRequest::Aws { access_key, secret_key, session_token, fingerprint } => {
Box::pin(async move {
let mapped = finish_mapping(
aws::map_access_with_credentials(
&access_key,
&secret_key,
session_token.as_deref(),
)
.await,
"aws",
&access_key,
);
(mapped, fingerprint)
})
}
AccessMapRequest::Gcp { credential_json, fingerprint } => Box::pin(async move {
let mapped = finish_mapping(
gcp::map_access_from_json(&credential_json).await,
"gcp",
"service_account",
);
(mapped, fingerprint)
}),
AccessMapRequest::GcpApiKey { api_key, fingerprint } => Box::pin(async move {
let mapped =
finish_mapping(gcp::map_access_from_api_key(&api_key).await, "gcp", "api_key");
(mapped, fingerprint)
}),
AccessMapRequest::Azure { credential_json, containers, fingerprint } => {
Box::pin(async move {
let mapped = finish_mapping(
azure::map_access_from_json_with_hints(&credential_json, containers.as_deref())
.await,
"azure",
"credential",
);
(mapped, fingerprint)
})
}
AccessMapRequest::AzureDevops { token, organization, fingerprint } => {
Box::pin(async move {
let mapped = finish_mapping(
azure_devops::map_access_from_token(&token, &organization).await,
"azure_devops",
"pat",
);
(mapped, fingerprint)
})
}
AccessMapRequest::Github { token, fingerprint } => {
Box::pin(async move { (map_token(&GithubMapper, &token).await, fingerprint) })
}
AccessMapRequest::Gitlab { token, fingerprint } => {
Box::pin(async move { (map_token(&GitlabMapper, &token).await, fingerprint) })
}
AccessMapRequest::Slack { token, fingerprint } => {
Box::pin(async move { (map_token(&SlackMapper, &token).await, fingerprint) })
}
AccessMapRequest::Postgres { uri, fingerprint } => Box::pin(async move {
let mapped =
finish_mapping(postgres::map_access_from_uri(&uri).await, "postgres", "uri");
(mapped, fingerprint)
}),
AccessMapRequest::MongoDB { uri, fingerprint } => Box::pin(async move {
let mapped = finish_mapping(mongodb::map_access_from_uri(&uri).await, "mongodb", "uri");
(mapped, fingerprint)
}),
AccessMapRequest::HuggingFace { token, fingerprint } => {
Box::pin(async move { (map_token(&HuggingFaceMapper, &token).await, fingerprint) })
}
AccessMapRequest::Gitea { token, fingerprint } => {
Box::pin(async move { (map_token(&GiteaMapper, &token).await, fingerprint) })
}
AccessMapRequest::Bitbucket { token, fingerprint } => {
Box::pin(async move { (map_token(&BitbucketMapper, &token).await, fingerprint) })
}
AccessMapRequest::Buildkite { token, fingerprint } => {
Box::pin(async move { (map_token(&BuildkiteMapper, &token).await, fingerprint) })
}
AccessMapRequest::Harness { token, fingerprint } => {
Box::pin(async move { (map_token(&HarnessMapper, &token).await, fingerprint) })
}
AccessMapRequest::OpenAI { token, fingerprint } => {
Box::pin(async move { (map_token(&OpenAiMapper, &token).await, fingerprint) })
}
AccessMapRequest::Anthropic { token, fingerprint } => {
Box::pin(async move { (map_token(&AnthropicMapper, &token).await, fingerprint) })
}
AccessMapRequest::Salesforce { token, instance, fingerprint } => Box::pin(async move {
let mapped = finish_mapping(
salesforce::map_access_from_token_and_instance(&token, &instance).await,
"salesforce",
"token",
);
(mapped, fingerprint)
}),
AccessMapRequest::WeightsAndBiases { token, fingerprint } => {
Box::pin(async move { (map_token(&WeightsAndBiasesMapper, &token).await, fingerprint) })
}
AccessMapRequest::MicrosoftTeams { webhook_url, fingerprint } => Box::pin(async move {
let mapped = finish_mapping(
microsoft_teams::map_access_from_webhook_url(&webhook_url).await,
"microsoft_teams",
"webhook",
);
(mapped, fingerprint)
}),
AccessMapRequest::Airtable { token, fingerprint } => {
Box::pin(async move { (map_token(&AirtableMapper, &token).await, fingerprint) })
}
AccessMapRequest::Alibaba { access_key, secret_key, session_token, fingerprint } => {
Box::pin(async move {
let mapped = finish_mapping(
alibaba::map_access_with_credentials(
&access_key,
&secret_key,
session_token.as_deref(),
)
.await,
"alibaba",
&access_key,
);
(mapped, fingerprint)
})
}
AccessMapRequest::CircleCI { token, fingerprint } => {
Box::pin(async move { (map_token(&CircleCiMapper, &token).await, fingerprint) })
}
AccessMapRequest::DigitalOcean { token, fingerprint } => {
Box::pin(async move { (map_token(&DigitalOceanMapper, &token).await, fingerprint) })
}
AccessMapRequest::Fastly { token, fingerprint } => {
Box::pin(async move { (map_token(&FastlyMapper, &token).await, fingerprint) })
}
AccessMapRequest::HubSpot { token, fingerprint } => {
Box::pin(async move { (map_token(&HubSpotMapper, &token).await, fingerprint) })
}
AccessMapRequest::IbmCloud { token, fingerprint } => {
Box::pin(async move { (map_token(&IbmCloudMapper, &token).await, fingerprint) })
}
AccessMapRequest::SendGrid { token, fingerprint } => {
Box::pin(async move { (map_token(&SendGridMapper, &token).await, fingerprint) })
}
AccessMapRequest::Sendinblue { token, fingerprint } => {
Box::pin(async move { (map_token(&SendinblueMapper, &token).await, fingerprint) })
}
AccessMapRequest::Stripe { token, fingerprint } => {
Box::pin(async move { (map_token(&StripeMapper, &token).await, fingerprint) })
}
AccessMapRequest::Terraform { token, fingerprint } => {
Box::pin(async move { (map_token(&TerraformMapper, &token).await, fingerprint) })
}
AccessMapRequest::Square { token, fingerprint } => {
Box::pin(async move { (map_token(&SquareMapper, &token).await, fingerprint) })
}
AccessMapRequest::Jira { token, base_url, fingerprint } => Box::pin(async move {
let mapped = finish_mapping(
jira::map_access_from_token_and_url(&token, &base_url).await,
"jira",
"token",
);
(mapped, fingerprint)
}),
AccessMapRequest::MySQL { uri, fingerprint } => Box::pin(async move {
let mapped = finish_mapping(mysql::map_access_from_uri(&uri).await, "mysql", "uri");
(mapped, fingerprint)
}),
AccessMapRequest::Algolia { app_id, api_key, fingerprint } => Box::pin(async move {
let mapped = finish_mapping(
algolia::map_access_from_credentials(&app_id, &api_key).await,
"algolia",
&app_id,
);
(mapped, fingerprint)
}),
AccessMapRequest::Auth0 { client_id, client_secret, domain, fingerprint } => {
Box::pin(async move {
let mapped = finish_mapping(
auth0::map_access_from_credentials(&client_id, &client_secret, &domain).await,
"auth0",
&client_id,
);
(mapped, fingerprint)
})
}
AccessMapRequest::PayPal { client_id, client_secret, fingerprint } => {
Box::pin(async move {
let mapped = finish_mapping(
paypal::map_access_from_credentials(&client_id, &client_secret).await,
"paypal",
&client_id,
);
(mapped, fingerprint)
})
}
AccessMapRequest::Plaid { client_id, secret, fingerprint } => Box::pin(async move {
let mapped = finish_mapping(
plaid::map_access_from_credentials(&client_id, &secret).await,
"plaid",
&client_id,
);
(mapped, fingerprint)
}),
AccessMapRequest::Shopify { token, subdomain, fingerprint } => Box::pin(async move {
let mapped = finish_mapping(
shopify::map_access_from_token_and_subdomain(&token, &subdomain).await,
"shopify",
&subdomain,
);
(mapped, fingerprint)
}),
AccessMapRequest::Zendesk { token, subdomain, fingerprint } => Box::pin(async move {
let mapped = finish_mapping(
zendesk::map_access_from_token_and_subdomain(&token, &subdomain).await,
"zendesk",
&subdomain,
);
(mapped, fingerprint)
}),
AccessMapRequest::Artifactory { token, base_url, fingerprint } => Box::pin(async move {
let mapped = finish_mapping(
match base_url {
Some(url) => artifactory::map_access_from_token_and_url(&token, &url).await,
None => artifactory::map_access_from_token(&token).await,
},
"artifactory",
"token",
);
(mapped, fingerprint)
}),
AccessMapRequest::Xray { token, base_url, fingerprint } => Box::pin(async move {
let mapped = finish_mapping(
match base_url {
Some(url) => xray::map_access_from_token_and_url(&token, &url).await,
None => xray::map_access_from_token(&token).await,
},
"jfrog_xray",
"token",
);
(mapped, fingerprint)
}),
AccessMapRequest::Monday { token, fingerprint } => {
Box::pin(async move { (map_token(&MondayMapper, &token).await, fingerprint) })
}
AccessMapRequest::Asana { token, fingerprint } => {
Box::pin(async move { (map_token(&AsanaMapper, &token).await, fingerprint) })
}
AccessMapRequest::Pinecone { token, fingerprint } => {
Box::pin(async move { (map_token(&PineconeMapper, &token).await, fingerprint) })
}
}
}
async fn map_token(mapper: &impl TokenAccessMapper, token: &str) -> AccessMapAttempt {
finish_mapping(mapper.map_access_from_token(token).await, mapper.cloud_name(), "token")
}
pub fn write_reports(results: &[AccessMapResult], html_out: &std::path::Path) -> Result<()> {
report::generate_html_report_multi(results, html_out)?;
Ok(())
}
pub async fn map_credential(args: &AccessMapArgs) -> Result<AccessMapResult> {
dispatch_cli_request(args).await
}
pub struct GithubMapper;
impl TokenAccessMapper for GithubMapper {
fn cloud_name(&self) -> &'static str {
"github"
}
async fn map_access_from_token(&self, token: &str) -> Result<AccessMapResult> {
github::map_access_from_token(token).await
}
}
pub struct GitlabMapper;
impl TokenAccessMapper for GitlabMapper {
fn cloud_name(&self) -> &'static str {
"gitlab"
}
async fn map_access_from_token(&self, token: &str) -> Result<AccessMapResult> {
gitlab::map_access_from_token(token).await
}
}
pub struct SlackMapper;
impl TokenAccessMapper for SlackMapper {
fn cloud_name(&self) -> &'static str {
"slack"
}
async fn map_access_from_token(&self, token: &str) -> Result<AccessMapResult> {
slack::map_access_from_token(token).await
}
}
pub struct HuggingFaceMapper;
impl TokenAccessMapper for HuggingFaceMapper {
fn cloud_name(&self) -> &'static str {
"huggingface"
}
async fn map_access_from_token(&self, token: &str) -> Result<AccessMapResult> {
huggingface::map_access_from_token(token).await
}
}
pub struct GiteaMapper;
impl TokenAccessMapper for GiteaMapper {
fn cloud_name(&self) -> &'static str {
"gitea"
}
async fn map_access_from_token(&self, token: &str) -> Result<AccessMapResult> {
gitea::map_access_from_token(token).await
}
}
pub struct BitbucketMapper;
impl TokenAccessMapper for BitbucketMapper {
fn cloud_name(&self) -> &'static str {
"bitbucket"
}
async fn map_access_from_token(&self, token: &str) -> Result<AccessMapResult> {
bitbucket::map_access_from_token(token).await
}
}
pub struct BuildkiteMapper;
impl TokenAccessMapper for BuildkiteMapper {
fn cloud_name(&self) -> &'static str {
"buildkite"
}
async fn map_access_from_token(&self, token: &str) -> Result<AccessMapResult> {
buildkite::map_access_from_token(token).await
}
}
pub struct HarnessMapper;
impl TokenAccessMapper for HarnessMapper {
fn cloud_name(&self) -> &'static str {
"harness"
}
async fn map_access_from_token(&self, token: &str) -> Result<AccessMapResult> {
harness::map_access_from_token(token).await
}
}
pub struct OpenAiMapper;
impl TokenAccessMapper for OpenAiMapper {
fn cloud_name(&self) -> &'static str {
"openai"
}
async fn map_access_from_token(&self, token: &str) -> Result<AccessMapResult> {
openai::map_access_from_token(token).await
}
}
pub struct AnthropicMapper;
impl TokenAccessMapper for AnthropicMapper {
fn cloud_name(&self) -> &'static str {
"anthropic"
}
async fn map_access_from_token(&self, token: &str) -> Result<AccessMapResult> {
anthropic::map_access_from_token(token).await
}
}
pub struct WeightsAndBiasesMapper;
impl TokenAccessMapper for WeightsAndBiasesMapper {
fn cloud_name(&self) -> &'static str {
"weightsandbiases"
}
async fn map_access_from_token(&self, token: &str) -> Result<AccessMapResult> {
weightsandbiases::map_access_from_token(token).await
}
}
pub struct AirtableMapper;
impl TokenAccessMapper for AirtableMapper {
fn cloud_name(&self) -> &'static str {
"airtable"
}
async fn map_access_from_token(&self, token: &str) -> Result<AccessMapResult> {
airtable::map_access_from_token(token).await
}
}
pub struct CircleCiMapper;
impl TokenAccessMapper for CircleCiMapper {
fn cloud_name(&self) -> &'static str {
"circleci"
}
async fn map_access_from_token(&self, token: &str) -> Result<AccessMapResult> {
circleci::map_access_from_token(token).await
}
}
pub struct DigitalOceanMapper;
impl TokenAccessMapper for DigitalOceanMapper {
fn cloud_name(&self) -> &'static str {
"digitalocean"
}
async fn map_access_from_token(&self, token: &str) -> Result<AccessMapResult> {
digitalocean::map_access_from_token(token).await
}
}
pub struct FastlyMapper;
impl TokenAccessMapper for FastlyMapper {
fn cloud_name(&self) -> &'static str {
"fastly"
}
async fn map_access_from_token(&self, token: &str) -> Result<AccessMapResult> {
fastly::map_access_from_token(token).await
}
}
pub struct HubSpotMapper;
impl TokenAccessMapper for HubSpotMapper {
fn cloud_name(&self) -> &'static str {
"hubspot"
}
async fn map_access_from_token(&self, token: &str) -> Result<AccessMapResult> {
hubspot::map_access_from_token(token).await
}
}
pub struct IbmCloudMapper;
impl TokenAccessMapper for IbmCloudMapper {
fn cloud_name(&self) -> &'static str {
"ibm_cloud"
}
async fn map_access_from_token(&self, token: &str) -> Result<AccessMapResult> {
ibm_cloud::map_access_from_token(token).await
}
}
pub struct SendGridMapper;
impl TokenAccessMapper for SendGridMapper {
fn cloud_name(&self) -> &'static str {
"sendgrid"
}
async fn map_access_from_token(&self, token: &str) -> Result<AccessMapResult> {
sendgrid::map_access_from_token(token).await
}
}
pub struct SendinblueMapper;
impl TokenAccessMapper for SendinblueMapper {
fn cloud_name(&self) -> &'static str {
"sendinblue"
}
async fn map_access_from_token(&self, token: &str) -> Result<AccessMapResult> {
sendinblue::map_access_from_token(token).await
}
}
pub struct StripeMapper;
impl TokenAccessMapper for StripeMapper {
fn cloud_name(&self) -> &'static str {
"stripe"
}
async fn map_access_from_token(&self, token: &str) -> Result<AccessMapResult> {
stripe::map_access_from_token(token).await
}
}
pub struct TerraformMapper;
impl TokenAccessMapper for TerraformMapper {
fn cloud_name(&self) -> &'static str {
"terraform"
}
async fn map_access_from_token(&self, token: &str) -> Result<AccessMapResult> {
terraform::map_access_from_token(token).await
}
}
pub struct SquareMapper;
impl TokenAccessMapper for SquareMapper {
fn cloud_name(&self) -> &'static str {
"square"
}
async fn map_access_from_token(&self, token: &str) -> Result<AccessMapResult> {
square::map_access_from_token(token).await
}
}
pub struct MondayMapper;
impl TokenAccessMapper for MondayMapper {
fn cloud_name(&self) -> &'static str {
"monday"
}
async fn map_access_from_token(&self, token: &str) -> Result<AccessMapResult> {
monday::map_access_from_token(token).await
}
}
pub struct AsanaMapper;
impl TokenAccessMapper for AsanaMapper {
fn cloud_name(&self) -> &'static str {
"asana"
}
async fn map_access_from_token(&self, token: &str) -> Result<AccessMapResult> {
asana::map_access_from_token(token).await
}
}
pub struct PineconeMapper;
impl TokenAccessMapper for PineconeMapper {
fn cloud_name(&self) -> &'static str {
"pinecone"
}
async fn map_access_from_token(&self, token: &str) -> Result<AccessMapResult> {
pinecone::map_access_from_token(token).await
}
}
fn severity_to_str(severity: Severity) -> &'static str {
match severity {
Severity::Low => "low",
Severity::Medium => "medium",
Severity::High => "high",
Severity::Critical => "critical",
}
}
fn build_failed_result(cloud: &str, identity_label: &str, err: anyhow::Error) -> AccessMapResult {
AccessMapResult {
cloud: cloud.to_string(),
identity: AccessSummary {
id: identity_label.to_string(),
access_type: "unknown".into(),
project: None,
tenant: None,
account_id: None,
},
roles: Vec::new(),
permissions: PermissionSummary::default(),
resources: vec![build_default_resource(None, Severity::Medium)],
severity: Severity::Medium,
recommendations: build_recommendations(Severity::Medium),
risk_notes: vec![format!("Identity mapping failed: {err}")],
token_details: None,
provider_metadata: None,
fingerprint: None,
}
}
pub(crate) fn build_default_resource(
project_id: Option<&str>,
severity: Severity,
) -> ResourceExposure {
ResourceExposure {
resource_type: "project".into(),
name: project_id.unwrap_or_default().into(),
permissions: Vec::new(),
risk: severity_to_str(severity).to_string(),
reason: "Project containing the provided credential".into(),
}
}
pub(crate) fn build_default_account_resource(
account_id: Option<&str>,
severity: Severity,
) -> ResourceExposure {
ResourceExposure {
resource_type: "account".into(),
name: account_id.unwrap_or_default().into(),
permissions: Vec::new(),
risk: severity_to_str(severity).to_string(),
reason: "AWS account linked to the provided credential".into(),
}
}
pub(crate) fn build_recommendations(severity: Severity) -> Vec<String> {
let mut recs = vec![
"Rotate the credential and audit recent usage".to_string(),
"Apply the principle of least privilege to attached roles".to_string(),
];
match severity {
Severity::Critical | Severity::High => {
recs.push("Investigate blast radius and revoke unused bindings".to_string())
}
Severity::Medium => {
recs.push("Review write-level permissions and tighten scopes".to_string())
}
Severity::Low => recs.push("Maintain monitoring for anomalous access".to_string()),
}
recs
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn provider_metadata_omits_empty_authorization_evidence() {
let metadata = ProviderMetadata::default();
let json = serde_json::to_value(metadata).unwrap();
assert!(json.get("authorization_evidence").is_none());
}
#[test]
fn authorization_evidence_serializes_ordered_identity_path() {
let metadata = ProviderMetadata {
version: None,
enterprise: None,
authorization_evidence: Some(AuthorizationEvidence {
paths: vec![AuthorizationPath {
direction: Some("outbound".into()),
status: "potential".into(),
hops: vec![
AuthorizationHop {
from: "source".into(),
to: "intermediate".into(),
relationship: "can_assume_role".into(),
},
AuthorizationHop {
from: "intermediate".into(),
to: "target".into(),
relationship: "can_assume_role".into(),
},
],
evidence: vec!["policy#allow".into()],
conditions: Vec::new(),
}],
role_impacts: vec![RoleImpact {
target: Some("intermediate".into()),
role: "target".into(),
name: Some("TargetRole".into()),
status: "potential".into(),
hop_count: 2,
permissions: vec!["s3.getobject".into()],
grants: vec![AuthorizationGrant {
permissions: vec!["s3.getobject".into()],
resources: vec!["arn:aws:s3:::example/*".into()],
evidence: vec!["policy#objects".into()],
..AuthorizationGrant::default()
}],
}],
..AuthorizationEvidence::default()
}),
};
let json = serde_json::to_value(metadata).unwrap();
let hops = json["authorization_evidence"]["paths"][0]["hops"].as_array().unwrap();
assert_eq!(hops[0]["from"], "source");
assert_eq!(hops[1]["to"], "target");
assert_eq!(
json["authorization_evidence"]["role_impacts"][0]["grants"][0]["resources"][0],
"arn:aws:s3:::example/*"
);
}
#[test]
fn authorization_evidence_serializes_read_only_probe_outcomes() {
let metadata = ProviderMetadata {
authorization_evidence: Some(AuthorizationEvidence {
probes: vec![AccessProbeEvidence {
service: "translate.googleapis.com".into(),
method: "language.languages.list".into(),
status: "restricted".into(),
http_status: Some(403),
reason: Some("API_KEY_SERVICE_BLOCKED".into()),
}],
..AuthorizationEvidence::default()
}),
..ProviderMetadata::default()
};
let json = serde_json::to_value(metadata).unwrap();
let probe = &json["authorization_evidence"]["probes"][0];
assert_eq!(probe["service"], "translate.googleapis.com");
assert_eq!(probe["status"], "restricted");
assert_eq!(probe["http_status"], 403);
}
}