use crate::{
AzureHttpClient, Result,
ops::security::SecurityOps,
types::security::{
Alert, AlertListResult, Assessment, AssessmentListResult, SecureScore,
SecureScoreListResult,
},
};
pub struct SecurityClient<'a> {
ops: SecurityOps<'a>,
client: &'a AzureHttpClient,
}
impl<'a> SecurityClient<'a> {
pub(crate) fn new(client: &'a AzureHttpClient) -> Self {
Self {
ops: SecurityOps::new(client),
client,
}
}
pub async fn list_alerts(&self) -> Result<AlertListResult> {
self.ops.list_alerts(self.client.subscription_id()).await
}
pub async fn get_alert(
&self,
resource_group_name: &str,
asc_location: &str,
alert_name: &str,
) -> Result<Alert> {
self.ops
.get_alert(
self.client.subscription_id(),
resource_group_name,
asc_location,
alert_name,
)
.await
}
pub async fn update_alert_status(
&self,
resource_group_name: &str,
asc_location: &str,
alert_name: &str,
alert_update_action_type: &str,
) -> Result<()> {
self.ops
.update_alert_status(
self.client.subscription_id(),
resource_group_name,
asc_location,
alert_name,
alert_update_action_type,
)
.await
}
pub async fn list_secure_scores(&self) -> Result<SecureScoreListResult> {
self.ops
.list_secure_scores(self.client.subscription_id())
.await
}
pub async fn get_secure_score(&self, secure_score_name: &str) -> Result<SecureScore> {
self.ops
.get_secure_score(self.client.subscription_id(), secure_score_name)
.await
}
pub async fn list_assessments(&self) -> Result<AssessmentListResult> {
self.ops
.list_assessments(self.client.subscription_id())
.await
}
pub async fn get_assessment(&self, assessment_name: &str) -> Result<Assessment> {
self.ops
.get_assessment(self.client.subscription_id(), assessment_name)
.await
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::MockClient;
const SUB_ID: &str = "test-subscription-id";
const ALERT_NAME: &str = "2517376_cloud-lite-test-alert";
const ASC_LOCATION: &str = "eastus";
const RG: &str = "test-rg";
const ASSESSMENT_NAME: &str = "050ac097-3dda-4d24-ab6d-82568e7a50cf";
fn make_client(mock: MockClient) -> AzureHttpClient {
AzureHttpClient::from_mock(mock)
}
fn alert_json() -> serde_json::Value {
serde_json::json!({
"id": format!("/subscriptions/{SUB_ID}/resourceGroups/{RG}/providers/Microsoft.Security/locations/{ASC_LOCATION}/alerts/{ALERT_NAME}"),
"name": ALERT_NAME,
"type": "Microsoft.Security/alerts",
"properties": {
"alertDisplayName": "Suspicious PowerShell activity",
"alertType": "VM_SuspiciousPowerShellActivity",
"severity": "High",
"status": "Active",
"description": "A suspicious PowerShell command was executed.",
"remediationSteps": ["Investigate the activity", "Revoke credentials"],
"intent": "Execution"
}
})
}
fn secure_score_json() -> serde_json::Value {
serde_json::json!({
"id": format!("/subscriptions/{SUB_ID}/providers/Microsoft.Security/secureScores/ascScore"),
"name": "ascScore",
"type": "Microsoft.Security/secureScores",
"properties": {
"displayName": "ASC score",
"score": {
"max": 21,
"current": 12.55,
"percentage": 0.5976
},
"weight": 1
}
})
}
fn assessment_json() -> serde_json::Value {
serde_json::json!({
"id": format!("/subscriptions/{SUB_ID}/providers/Microsoft.Security/assessments/{ASSESSMENT_NAME}"),
"name": ASSESSMENT_NAME,
"type": "Microsoft.Security/assessments",
"properties": {
"displayName": "Disabled accounts with owner permissions should be removed",
"status": {
"code": "Healthy",
"cause": "OffByPolicy",
"description": "All accounts are enabled"
}
}
})
}
#[tokio::test]
async fn list_alerts_returns_list() {
let mut mock = MockClient::new();
mock.expect_get(&format!(
"/subscriptions/{SUB_ID}/providers/Microsoft.Security/alerts"
))
.returning_json(serde_json::json!({ "value": [alert_json()] }));
let client = make_client(mock);
let result = client
.security()
.list_alerts()
.await
.expect("list_alerts failed");
assert_eq!(result.value.len(), 1);
let a = &result.value[0];
assert_eq!(a.name.as_deref(), Some(ALERT_NAME));
let props = a.properties.as_ref().unwrap();
assert_eq!(props.severity.as_deref(), Some("High"));
assert_eq!(props.status.as_deref(), Some("Active"));
assert_eq!(props.remediation_steps.len(), 2);
}
#[tokio::test]
async fn get_alert_deserializes_properties() {
let mut mock = MockClient::new();
mock.expect_get(
&format!("/subscriptions/{SUB_ID}/resourceGroups/{RG}/providers/Microsoft.Security/locations/{ASC_LOCATION}/alerts/{ALERT_NAME}"),
)
.returning_json(alert_json());
let client = make_client(mock);
let a = client
.security()
.get_alert(RG, ASC_LOCATION, ALERT_NAME)
.await
.expect("get_alert failed");
assert_eq!(a.name.as_deref(), Some(ALERT_NAME));
let props = a.properties.as_ref().unwrap();
assert_eq!(
props.alert_type.as_deref(),
Some("VM_SuspiciousPowerShellActivity")
);
assert_eq!(props.intent.as_deref(), Some("Execution"));
}
#[tokio::test]
async fn update_alert_status_constructs_url() {
let mut mock = MockClient::new();
mock.expect_post(
&format!("/subscriptions/{SUB_ID}/resourceGroups/{RG}/providers/Microsoft.Security/locations/{ASC_LOCATION}/alerts/{ALERT_NAME}/dismiss"),
)
.returning_json(serde_json::json!({}));
let client = make_client(mock);
client
.security()
.update_alert_status(RG, ASC_LOCATION, ALERT_NAME, "dismiss")
.await
.expect("update_alert_status failed");
}
#[tokio::test]
async fn list_secure_scores_returns_asc_score() {
let mut mock = MockClient::new();
mock.expect_get(&format!(
"/subscriptions/{SUB_ID}/providers/Microsoft.Security/secureScores"
))
.returning_json(serde_json::json!({ "value": [secure_score_json()] }));
let client = make_client(mock);
let result = client
.security()
.list_secure_scores()
.await
.expect("list_secure_scores failed");
assert_eq!(result.value.len(), 1);
let score = &result.value[0];
assert_eq!(score.name.as_deref(), Some("ascScore"));
let props = score.properties.as_ref().unwrap();
let details = props.score.as_ref().unwrap();
assert_eq!(details.max, Some(21));
assert_eq!(details.current, Some(12.55));
}
#[tokio::test]
async fn get_assessment_deserializes_status() {
let mut mock = MockClient::new();
mock.expect_get(&format!(
"/subscriptions/{SUB_ID}/providers/Microsoft.Security/assessments/{ASSESSMENT_NAME}"
))
.returning_json(assessment_json());
let client = make_client(mock);
let a = client
.security()
.get_assessment(ASSESSMENT_NAME)
.await
.expect("get_assessment failed");
assert_eq!(a.name.as_deref(), Some(ASSESSMENT_NAME));
let props = a.properties.as_ref().unwrap();
let status = props.status.as_ref().unwrap();
assert_eq!(status.code.as_deref(), Some("Healthy"));
assert_eq!(status.cause.as_deref(), Some("OffByPolicy"));
}
}