Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -68,5 +68,62 @@ dirs-next = "2"
libc = "0.2"
calamine = "0.26"

[features]
azure = [
"dep:azure_core",
"dep:azure_identity",
"dep:azure_mgmt_containerregistry",
"dep:azure_mgmt_web",
"dep:azure_mgmt_security",
"dep:azure_mgmt_keyvault",
"dep:azure_mgmt_policyinsights",
"dep:azure_mgmt_network",
"dep:azure_mgmt_monitor",
"dep:futures",
]

[dependencies.azure_core]
version = "0.21"
features = ["enable_reqwest"]
optional = true

[dependencies.azure_identity]
version = "0.21"
optional = true

[dependencies.azure_mgmt_containerregistry]
version = "0.21"
optional = true

[dependencies.azure_mgmt_web]
version = "0.21"
optional = true

[dependencies.azure_mgmt_security]
version = "0.21"
optional = true

[dependencies.azure_mgmt_keyvault]
version = "0.21"
optional = true

[dependencies.azure_mgmt_policyinsights]
version = "0.21"
optional = true

[dependencies.azure_mgmt_network]
version = "0.21"
optional = true

[dependencies.azure_mgmt_monitor]
version = "0.21"
features = ["package-preview-2023-09"]
default-features = false
optional = true

[dependencies.futures]
version = "0.3"
optional = true

[dev-dependencies]
tempfile = "3"
83 changes: 79 additions & 4 deletions src/app_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,28 +72,65 @@ pub struct CollectorConfig {
pub enable_extra: Option<Vec<String>>,
}

/// A named AWS account that the tool can collect evidence from.
/// Cloud provider tag for an account entry.
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum CloudProvider {
Aws,
Azure,
Gcp,
}

impl Default for CloudProvider {
fn default() -> Self {
CloudProvider::Aws
}
}

fn default_provider() -> CloudProvider {
CloudProvider::Aws
}

fn default_profile() -> String {
String::new()
}

/// A named cloud account that the tool can collect evidence from.
///
/// Each account maps to an AWS CLI profile (typically an SSO role)
/// and carries its own region, output directory, and collector
/// override settings.
/// Each account maps to credentials and carries its own region, output
/// directory, and collector override settings.
#[derive(Debug, Clone, Deserialize)]
pub struct Account {
/// Human-readable display name (e.g. "Corporate Production").
pub name: String,

/// Cloud provider for this account entry. Defaults to `aws`.
#[serde(default = "default_provider")]
pub provider: CloudProvider,
Comment on lines +107 to +109

// ── AWS ──────────────────────────────────────────────────────────────────
/// AWS account ID, shown in the TUI for identification.
pub account_id: Option<String>,

/// Short description shown below the account name.
pub description: Option<String>,

/// AWS CLI profile name or SSO role name (must match ~/.aws/config).
/// Non-AWS account entries can omit this field.
#[serde(default = "default_profile")]
pub profile: String,

/// Override the default region for this account.
pub region: Option<String>,

// ── Azure ─────────────────────────────────────────────────────────────────
/// Azure Active Directory tenant ID (UUID).
pub tenant_id: Option<String>,

/// Azure subscription ID (UUID) to collect from.
pub subscription_id: Option<String>,

// ── Shared ────────────────────────────────────────────────────────────────
/// Override the default output directory for this account.
pub output_dir: Option<String>,

Expand Down Expand Up @@ -124,3 +161,41 @@ fn global_config_path() -> Option<PathBuf> {
let base = dirs_next::home_dir()?;
Some(base.join(".config").join("evidence").join("config.toml"))
}

#[cfg(test)]
mod tests {
use super::{AppConfig, CloudProvider};

#[test]
fn deserializes_azure_account_without_aws_profile() {
let config: AppConfig = toml::from_str(
r#"
[[account]]
name = "Azure Prod"
provider = "azure"
tenant_id = "tenant-id"
subscription_id = "subscription-id"
"#,
)
.expect("config should deserialize");

let account = &config.account[0];
assert_eq!(account.provider, CloudProvider::Azure);
assert!(account.profile.is_empty());
}

#[test]
fn defaults_provider_to_aws() {
let config: AppConfig = toml::from_str(
r#"
[[account]]
name = "AWS Prod"
profile = "prod"
"#,
)
.expect("config should deserialize");

assert_eq!(config.account[0].provider, CloudProvider::Aws);
assert_eq!(config.account[0].profile, "prod");
}
}
3 changes: 3 additions & 0 deletions src/evidence.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,4 +138,7 @@ pub enum EvidenceSource {
BackupApi,
RdsApi,
CloudTrailS3,
AzureActivityLog,
AzureSecurityCenter,
GcpAuditLog,
}
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
mod providers;
mod access_analyzer;
mod account_config;
mod acm;
Expand Down
102 changes: 102 additions & 0 deletions src/providers/azure/acr.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
//! Azure Container Registry (ACR) collector.
//!
//! Maps to AWS ECR. Uses `azure_mgmt_containerregistry` to list all registries
//! in the subscription.

use std::sync::Arc;

use anyhow::{Context, Result};
use async_trait::async_trait;
use azure_mgmt_containerregistry::Client as AcrClient;
use futures::StreamExt;

use crate::evidence::CsvCollector;
Comment thread
Copilot marked this conversation as resolved.

pub struct AcrCollector {
client: AcrClient,
subscription_id: String,
}

impl AcrCollector {
pub fn new(
credential: Arc<dyn azure_core::auth::TokenCredential>,
subscription_id: String,
) -> Self {
Self {
client: AcrClient::builder(credential).build(),
subscription_id,
}
}
Comment thread
Copilot marked this conversation as resolved.
}

#[async_trait]
impl CsvCollector for AcrCollector {
fn name(&self) -> &str {
"Azure Container Registry"
}
fn filename_prefix(&self) -> &str {
"Azure_Container_Registries"
}

fn headers(&self) -> &'static [&'static str] {
&[
"Registry Name",
"Resource Group",
"Location",
"SKU",
"Admin User Enabled",
"Login Server",
"Provisioning State",
]
}

async fn collect_rows(
&self,
_account_id: &str,
_region: &str,
_dates: Option<(i64, i64)>,
) -> Result<Vec<Vec<String>>> {
let mut rows = Vec::new();

let mut stream = self
.client
.registries_client()
.list(&self.subscription_id)
.into_stream();

while let Some(page) = stream.next().await {
let page = page.context("ACR: list page failed")?;
for reg in page.value {
let props = reg.properties.as_ref();
let rg = reg
.resource
.id
.as_deref()
.and_then(|id| id.split("/resourceGroups/").nth(1))
.and_then(|s| s.split('/').next())
.unwrap_or("")
.to_string();

rows.push(vec![
reg.resource.name.clone().unwrap_or_default(),
rg,
reg.resource.location.clone(),
format!("{:?}", reg.sku.name),
props
.and_then(|p| p.admin_user_enabled)
.map(|b| b.to_string())
.unwrap_or_default(),
props
.and_then(|p| p.login_server.clone())
.unwrap_or_default(),
props
.and_then(|p| p.provisioning_state.as_ref())
.map(|s| format!("{:?}", s))
.unwrap_or_default(),
]);
}
}

Ok(rows)
}
}
Loading