* feat(tools): add cloud transformation accelerator tools Add cloud_ops and cloud_patterns tools providing read-only cloud transformation analysis: IaC review, migration assessment, cost analysis, and Well-Architected Framework architecture review. Includes CloudOpsConfig, SecurityOpsConfig, and ConversationalAiConfig schema additions, Box::pin fixes for recursive async in cron scheduler, and approval_manager field in ChannelRuntimeContext test constructors. Original work by @rareba. Rebased on latest master with conflict resolution (kept SwarmConfig/SwarmStrategy exports, swarm tool registration, and approval_manager in test constructors). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * style: cargo fmt Box::pin calls Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
852 lines
29 KiB
Rust
852 lines
29 KiB
Rust
//! Cloud operations advisory tool for cloud transformation analysis.
|
|
//!
|
|
//! Provides read-only analysis capabilities: IaC review, migration assessment,
|
|
//! cost analysis, and Well-Architected Framework architecture review.
|
|
//! This tool does NOT create, modify, or delete cloud resources.
|
|
|
|
use super::traits::{Tool, ToolResult};
|
|
use crate::config::CloudOpsConfig;
|
|
use crate::util::truncate_with_ellipsis;
|
|
use async_trait::async_trait;
|
|
use serde_json::json;
|
|
|
|
/// Read-only cloud operations advisory tool.
|
|
///
|
|
/// Actions: `review_iac`, `assess_migration`, `cost_analysis`, `architecture_review`.
|
|
pub struct CloudOpsTool {
|
|
config: CloudOpsConfig,
|
|
}
|
|
|
|
impl CloudOpsTool {
|
|
pub fn new(config: CloudOpsConfig) -> Self {
|
|
Self { config }
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl Tool for CloudOpsTool {
|
|
fn name(&self) -> &str {
|
|
"cloud_ops"
|
|
}
|
|
|
|
fn description(&self) -> &str {
|
|
"Cloud transformation advisory tool. Analyzes IaC plans, assesses migration paths, \
|
|
reviews costs, and checks architecture against Well-Architected Framework pillars. \
|
|
Read-only: does not create or modify cloud resources."
|
|
}
|
|
|
|
fn parameters_schema(&self) -> serde_json::Value {
|
|
json!({
|
|
"type": "object",
|
|
"properties": {
|
|
"action": {
|
|
"type": "string",
|
|
"enum": ["review_iac", "assess_migration", "cost_analysis", "architecture_review"],
|
|
"description": "The analysis action to perform."
|
|
},
|
|
"input": {
|
|
"type": "string",
|
|
"description": "For review_iac: IaC plan text or JSON content to analyze. For assess_migration: current architecture description text. For cost_analysis: billing data as CSV/JSON text. For architecture_review: architecture description text. Note: provide text content directly, not file paths."
|
|
},
|
|
"cloud": {
|
|
"type": "string",
|
|
"description": "Target cloud provider (aws, azure, gcp). Uses configured default if omitted."
|
|
}
|
|
},
|
|
"required": ["action", "input"]
|
|
})
|
|
}
|
|
|
|
async fn execute(&self, args: serde_json::Value) -> anyhow::Result<ToolResult> {
|
|
let action = match args.get("action") {
|
|
Some(v) => v
|
|
.as_str()
|
|
.ok_or_else(|| anyhow::anyhow!("'action' must be a string, got: {}", v))?,
|
|
None => {
|
|
return Ok(ToolResult {
|
|
success: false,
|
|
output: String::new(),
|
|
error: Some("'action' parameter is required".into()),
|
|
});
|
|
}
|
|
};
|
|
let input = match args.get("input") {
|
|
Some(v) => v
|
|
.as_str()
|
|
.ok_or_else(|| anyhow::anyhow!("'input' must be a string, got: {}", v))?,
|
|
None => "",
|
|
};
|
|
let cloud = match args.get("cloud") {
|
|
Some(v) => v
|
|
.as_str()
|
|
.ok_or_else(|| anyhow::anyhow!("'cloud' must be a string, got: {}", v))?,
|
|
None => &self.config.default_cloud,
|
|
};
|
|
|
|
if input.is_empty() {
|
|
return Ok(ToolResult {
|
|
success: false,
|
|
output: String::new(),
|
|
error: Some("'input' parameter is required and cannot be empty".into()),
|
|
});
|
|
}
|
|
|
|
if !self.config.supported_clouds.contains(&cloud.to_string()) {
|
|
return Ok(ToolResult {
|
|
success: false,
|
|
output: String::new(),
|
|
error: Some(format!(
|
|
"Cloud provider '{}' is not in supported_clouds: {:?}",
|
|
cloud, self.config.supported_clouds
|
|
)),
|
|
});
|
|
}
|
|
|
|
match action {
|
|
"review_iac" => self.review_iac(input, cloud).await,
|
|
"assess_migration" => self.assess_migration(input, cloud).await,
|
|
"cost_analysis" => self.cost_analysis(input, cloud).await,
|
|
"architecture_review" => self.architecture_review(input, cloud).await,
|
|
_ => Ok(ToolResult {
|
|
success: false,
|
|
output: String::new(),
|
|
error: Some(format!(
|
|
"Unknown action '{}'. Valid: review_iac, assess_migration, cost_analysis, architecture_review",
|
|
action
|
|
)),
|
|
}),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[allow(clippy::unused_async)]
|
|
impl CloudOpsTool {
|
|
async fn review_iac(&self, input: &str, cloud: &str) -> anyhow::Result<ToolResult> {
|
|
let mut findings = Vec::new();
|
|
|
|
// Detect IaC type from content
|
|
let iac_type = detect_iac_type(input);
|
|
|
|
// Security findings
|
|
for finding in scan_iac_security(input) {
|
|
findings.push(finding);
|
|
}
|
|
|
|
// Best practice findings
|
|
for finding in scan_iac_best_practices(input, cloud) {
|
|
findings.push(finding);
|
|
}
|
|
|
|
// Cost implications
|
|
for finding in scan_iac_cost(input, cloud, self.config.cost_threshold_monthly_usd) {
|
|
findings.push(finding);
|
|
}
|
|
|
|
let output = json!({
|
|
"iac_type": iac_type,
|
|
"cloud": cloud,
|
|
"findings_count": findings.len(),
|
|
"findings": findings,
|
|
"supported_iac_tools": self.config.iac_tools,
|
|
});
|
|
|
|
Ok(ToolResult {
|
|
success: true,
|
|
output: serde_json::to_string_pretty(&output)?,
|
|
error: None,
|
|
})
|
|
}
|
|
|
|
async fn assess_migration(&self, input: &str, cloud: &str) -> anyhow::Result<ToolResult> {
|
|
let recommendations = assess_migration_recommendations(input, cloud);
|
|
|
|
let output = json!({
|
|
"cloud": cloud,
|
|
"source_description": truncate_with_ellipsis(input, 200),
|
|
"recommendations": recommendations,
|
|
});
|
|
|
|
Ok(ToolResult {
|
|
success: true,
|
|
output: serde_json::to_string_pretty(&output)?,
|
|
error: None,
|
|
})
|
|
}
|
|
|
|
async fn cost_analysis(&self, input: &str, cloud: &str) -> anyhow::Result<ToolResult> {
|
|
let opportunities =
|
|
analyze_cost_opportunities(input, self.config.cost_threshold_monthly_usd);
|
|
|
|
let output = json!({
|
|
"cloud": cloud,
|
|
"threshold_usd": self.config.cost_threshold_monthly_usd,
|
|
"opportunities_count": opportunities.len(),
|
|
"opportunities": opportunities,
|
|
});
|
|
|
|
Ok(ToolResult {
|
|
success: true,
|
|
output: serde_json::to_string_pretty(&output)?,
|
|
error: None,
|
|
})
|
|
}
|
|
|
|
async fn architecture_review(&self, input: &str, cloud: &str) -> anyhow::Result<ToolResult> {
|
|
let frameworks = &self.config.well_architected_frameworks;
|
|
let pillars = review_architecture_pillars(input, cloud, frameworks);
|
|
|
|
let output = json!({
|
|
"cloud": cloud,
|
|
"frameworks": frameworks,
|
|
"pillars": pillars,
|
|
});
|
|
|
|
Ok(ToolResult {
|
|
success: true,
|
|
output: serde_json::to_string_pretty(&output)?,
|
|
error: None,
|
|
})
|
|
}
|
|
}
|
|
|
|
// ── Analysis helpers ──────────────────────────────────────────────
|
|
|
|
fn detect_iac_type(input: &str) -> &'static str {
|
|
let lower = input.to_lowercase();
|
|
if lower.contains("resource \"") || lower.contains("terraform") || lower.contains(".tf") {
|
|
"terraform"
|
|
} else if lower.contains("awstemplatebody")
|
|
|| lower.contains("cloudformation")
|
|
|| lower.contains("aws::")
|
|
{
|
|
"cloudformation"
|
|
} else if lower.contains("pulumi") {
|
|
"pulumi"
|
|
} else {
|
|
"unknown"
|
|
}
|
|
}
|
|
|
|
/// Scan IaC content for common security issues.
|
|
fn scan_iac_security(input: &str) -> Vec<serde_json::Value> {
|
|
let lower = input.to_lowercase();
|
|
let mut findings = Vec::new();
|
|
|
|
let security_patterns: &[(&str, &str, &str)] = &[
|
|
(
|
|
"0.0.0.0/0",
|
|
"high",
|
|
"Unrestricted ingress (0.0.0.0/0) detected. Restrict CIDR ranges to known networks.",
|
|
),
|
|
(
|
|
"::/0",
|
|
"high",
|
|
"Unrestricted IPv6 ingress (::/0) detected. Restrict CIDR ranges.",
|
|
),
|
|
(
|
|
"public_access",
|
|
"medium",
|
|
"Public access setting detected. Verify this is intentional and necessary.",
|
|
),
|
|
(
|
|
"publicly_accessible",
|
|
"medium",
|
|
"Resource marked as publicly accessible. Ensure this is required.",
|
|
),
|
|
(
|
|
"encrypted = false",
|
|
"high",
|
|
"Encryption explicitly disabled. Enable encryption at rest.",
|
|
),
|
|
(
|
|
"\"*\"",
|
|
"medium",
|
|
"Wildcard permission detected. Follow least-privilege principle.",
|
|
),
|
|
(
|
|
"password",
|
|
"medium",
|
|
"Hardcoded password reference detected. Use secrets manager instead.",
|
|
),
|
|
(
|
|
"access_key",
|
|
"high",
|
|
"Access key reference in IaC. Use IAM roles or secrets manager.",
|
|
),
|
|
(
|
|
"secret_key",
|
|
"high",
|
|
"Secret key reference in IaC. Use IAM roles or secrets manager.",
|
|
),
|
|
];
|
|
|
|
for (pattern, severity, message) in security_patterns {
|
|
if lower.contains(pattern) {
|
|
findings.push(json!({
|
|
"category": "security",
|
|
"severity": severity,
|
|
"message": message,
|
|
}));
|
|
}
|
|
}
|
|
|
|
findings
|
|
}
|
|
|
|
/// Scan for IaC best practice violations.
|
|
fn scan_iac_best_practices(input: &str, cloud: &str) -> Vec<serde_json::Value> {
|
|
let lower = input.to_lowercase();
|
|
let mut findings = Vec::new();
|
|
|
|
// Tagging
|
|
if !lower.contains("tags") && !lower.contains("tag") {
|
|
findings.push(json!({
|
|
"category": "best_practice",
|
|
"severity": "low",
|
|
"message": "No resource tags detected. Add tags for cost allocation and resource management.",
|
|
}));
|
|
}
|
|
|
|
// Versioning
|
|
if lower.contains("s3") && !lower.contains("versioning") {
|
|
findings.push(json!({
|
|
"category": "best_practice",
|
|
"severity": "medium",
|
|
"message": "S3 bucket without versioning detected. Enable versioning for data protection.",
|
|
}));
|
|
}
|
|
|
|
// Logging
|
|
if !lower.contains("logging") && !lower.contains("log_group") && !lower.contains("access_logs")
|
|
{
|
|
findings.push(json!({
|
|
"category": "best_practice",
|
|
"severity": "low",
|
|
"message": format!("No logging configuration detected for {}. Enable access logging.", cloud),
|
|
}));
|
|
}
|
|
|
|
// Backup
|
|
if lower.contains("rds") && !lower.contains("backup_retention") {
|
|
findings.push(json!({
|
|
"category": "best_practice",
|
|
"severity": "medium",
|
|
"message": "RDS instance without backup retention configuration. Set backup_retention_period.",
|
|
}));
|
|
}
|
|
|
|
findings
|
|
}
|
|
|
|
/// Scan for cost-related observations in IaC.
|
|
///
|
|
/// Only emits findings for resources whose estimated monthly cost exceeds
|
|
/// `threshold`. AWS-specific patterns (NAT Gateway, Elastic IP, ALB) are
|
|
/// gated behind `cloud == "aws"`.
|
|
fn scan_iac_cost(input: &str, cloud: &str, threshold: f64) -> Vec<serde_json::Value> {
|
|
let lower = input.to_lowercase();
|
|
let mut findings = Vec::new();
|
|
|
|
// (pattern, message, estimated_monthly_usd, aws_only)
|
|
let expensive_patterns: &[(&str, &str, f64, bool)] = &[
|
|
("instance_type", "Review instance sizing. Consider right-sizing or spot/preemptible instances.", 50.0, false),
|
|
("nat_gateway", "NAT Gateway detected. These incur hourly + data transfer charges. Consider VPC endpoints for AWS services.", 45.0, true),
|
|
("elastic_ip", "Elastic IP detected. Unused EIPs incur charges.", 5.0, true),
|
|
("load_balancer", "Load balancer detected. Verify it is needed; consider ALB over NLB/CLB for cost.", 25.0, true),
|
|
];
|
|
|
|
for (pattern, message, estimated_cost, aws_only) in expensive_patterns {
|
|
if *aws_only && cloud != "aws" {
|
|
continue;
|
|
}
|
|
if *estimated_cost < threshold {
|
|
continue;
|
|
}
|
|
if lower.contains(pattern) {
|
|
findings.push(json!({
|
|
"category": "cost",
|
|
"severity": "info",
|
|
"message": message,
|
|
"estimated_monthly_usd": estimated_cost,
|
|
}));
|
|
}
|
|
}
|
|
|
|
findings
|
|
}
|
|
|
|
/// Generate migration recommendations based on architecture description.
|
|
fn assess_migration_recommendations(input: &str, cloud: &str) -> Vec<serde_json::Value> {
|
|
let lower = input.to_lowercase();
|
|
let mut recs = Vec::new();
|
|
|
|
let migration_patterns: &[(&str, &str, &str, &str)] = &[
|
|
("monolith", "Decompose into microservices or modular containers.",
|
|
"high", "Consider containerizing with ECS/EKS (AWS), AKS (Azure), or GKE (GCP)."),
|
|
("vm", "Migrate VMs to containers or serverless where feasible.",
|
|
"medium", "Evaluate lift-and-shift to managed container services."),
|
|
("on-premises", "Assess workloads for cloud readiness using 6 Rs framework (rehost, replatform, refactor, repurchase, retire, retain).",
|
|
"high", "Start with rehost for quick migration, then optimize."),
|
|
("database", "Evaluate managed database services for reduced operational overhead.",
|
|
"medium", &format!("Consider managed options: RDS/Aurora (AWS), Azure SQL (Azure), Cloud SQL (GCP) for {}.", cloud)),
|
|
("batch", "Consider serverless compute for batch workloads.",
|
|
"low", "Evaluate Lambda (AWS), Azure Functions, or Cloud Functions for event-driven batch."),
|
|
("queue", "Evaluate managed message queue services.",
|
|
"low", "Consider SQS/SNS (AWS), Service Bus (Azure), or Pub/Sub (GCP)."),
|
|
("storage", "Evaluate tiered object storage for cost optimization.",
|
|
"medium", "Use lifecycle policies for infrequent access data."),
|
|
("legacy", "Assess modernization path: replatform or refactor.",
|
|
"high", "Legacy systems carry tech debt; prioritize incremental modernization."),
|
|
];
|
|
|
|
for (keyword, recommendation, effort, detail) in migration_patterns {
|
|
if lower.contains(keyword) {
|
|
recs.push(json!({
|
|
"trigger": keyword,
|
|
"recommendation": recommendation,
|
|
"effort_estimate": effort,
|
|
"detail": detail,
|
|
"target_cloud": cloud,
|
|
}));
|
|
}
|
|
}
|
|
|
|
if recs.is_empty() {
|
|
recs.push(json!({
|
|
"trigger": "general",
|
|
"recommendation": "Provide more detail about current architecture components for targeted recommendations.",
|
|
"effort_estimate": "unknown",
|
|
"detail": "Include details about compute, storage, networking, and data layers.",
|
|
"target_cloud": cloud,
|
|
}));
|
|
}
|
|
|
|
recs
|
|
}
|
|
|
|
/// Analyze billing/cost data for optimization opportunities.
|
|
fn analyze_cost_opportunities(input: &str, threshold: f64) -> Vec<serde_json::Value> {
|
|
let lower = input.to_lowercase();
|
|
let mut opportunities = Vec::new();
|
|
|
|
// General cost patterns
|
|
let cost_patterns: &[(&str, &str, &str)] = &[
|
|
("reserved", "Review reserved instance utilization. Unused reservations waste budget.", "high"),
|
|
("on-demand", "On-demand instances detected. Evaluate savings plans or reserved instances for stable workloads.", "high"),
|
|
("data transfer", "Data transfer costs detected. Use VPC endpoints, CDN, or regional placement to reduce.", "medium"),
|
|
("storage", "Storage costs detected. Implement lifecycle policies and tiered storage.", "medium"),
|
|
("idle", "Idle resources detected. Identify and terminate unused resources.", "high"),
|
|
("unattached", "Unattached resources (volumes, IPs) detected. Clean up to reduce waste.", "medium"),
|
|
("snapshot", "Snapshot costs detected. Review retention policies and delete stale snapshots.", "low"),
|
|
];
|
|
|
|
for (pattern, suggestion, priority) in cost_patterns {
|
|
if lower.contains(pattern) {
|
|
opportunities.push(json!({
|
|
"pattern": pattern,
|
|
"suggestion": suggestion,
|
|
"priority": priority,
|
|
"threshold_usd": threshold,
|
|
}));
|
|
}
|
|
}
|
|
|
|
if opportunities.is_empty() {
|
|
opportunities.push(json!({
|
|
"pattern": "general",
|
|
"suggestion": "Provide billing CSV/JSON data with service and cost columns for detailed analysis.",
|
|
"priority": "info",
|
|
"threshold_usd": threshold,
|
|
}));
|
|
}
|
|
|
|
opportunities
|
|
}
|
|
|
|
/// Review architecture against Well-Architected Framework pillars.
|
|
fn review_architecture_pillars(
|
|
input: &str,
|
|
cloud: &str,
|
|
_frameworks: &[String],
|
|
) -> Vec<serde_json::Value> {
|
|
let lower = input.to_lowercase();
|
|
|
|
let pillars = vec![
|
|
("security", review_pillar_security(&lower, cloud)),
|
|
("reliability", review_pillar_reliability(&lower, cloud)),
|
|
("performance", review_pillar_performance(&lower, cloud)),
|
|
("cost_optimization", review_pillar_cost(&lower, cloud)),
|
|
(
|
|
"operational_excellence",
|
|
review_pillar_operations(&lower, cloud),
|
|
),
|
|
];
|
|
|
|
pillars
|
|
.into_iter()
|
|
.map(|(name, findings)| {
|
|
json!({
|
|
"pillar": name,
|
|
"findings_count": findings.len(),
|
|
"findings": findings,
|
|
})
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
fn review_pillar_security(input: &str, _cloud: &str) -> Vec<String> {
|
|
let mut findings = Vec::new();
|
|
if !input.contains("iam") && !input.contains("identity") {
|
|
findings.push(
|
|
"No IAM/identity layer described. Define identity and access management strategy."
|
|
.into(),
|
|
);
|
|
}
|
|
if !input.contains("encrypt") {
|
|
findings
|
|
.push("No encryption mentioned. Implement encryption at rest and in transit.".into());
|
|
}
|
|
if !input.contains("firewall") && !input.contains("waf") && !input.contains("security group") {
|
|
findings.push(
|
|
"No network security controls described. Add WAF, security groups, or firewall rules."
|
|
.into(),
|
|
);
|
|
}
|
|
if !input.contains("audit") && !input.contains("logging") {
|
|
findings.push(
|
|
"No audit logging described. Enable CloudTrail/Azure Monitor/Cloud Audit Logs.".into(),
|
|
);
|
|
}
|
|
findings
|
|
}
|
|
|
|
fn review_pillar_reliability(input: &str, _cloud: &str) -> Vec<String> {
|
|
let mut findings = Vec::new();
|
|
if !input.contains("multi-az") && !input.contains("multi-region") && !input.contains("redundan")
|
|
{
|
|
findings
|
|
.push("No redundancy described. Consider multi-AZ or multi-region deployment.".into());
|
|
}
|
|
if !input.contains("backup") {
|
|
findings.push("No backup strategy described. Define RPO/RTO and backup schedules.".into());
|
|
}
|
|
if !input.contains("auto-scal") && !input.contains("autoscal") {
|
|
findings.push(
|
|
"No auto-scaling described. Implement scaling policies for variable load.".into(),
|
|
);
|
|
}
|
|
if !input.contains("health check") && !input.contains("monitor") {
|
|
findings.push("No health monitoring described. Add health checks and alerting.".into());
|
|
}
|
|
findings
|
|
}
|
|
|
|
fn review_pillar_performance(input: &str, _cloud: &str) -> Vec<String> {
|
|
let mut findings = Vec::new();
|
|
if !input.contains("cache") && !input.contains("cdn") {
|
|
findings
|
|
.push("No caching layer described. Consider CDN and application-level caching.".into());
|
|
}
|
|
if !input.contains("load balanc") {
|
|
findings
|
|
.push("No load balancing described. Add load balancer for distributed traffic.".into());
|
|
}
|
|
if !input.contains("metric") && !input.contains("benchmark") {
|
|
findings.push(
|
|
"No performance metrics described. Define SLIs/SLOs and baseline benchmarks.".into(),
|
|
);
|
|
}
|
|
findings
|
|
}
|
|
|
|
fn review_pillar_cost(input: &str, _cloud: &str) -> Vec<String> {
|
|
let mut findings = Vec::new();
|
|
if !input.contains("budget") && !input.contains("cost") {
|
|
findings
|
|
.push("No cost controls described. Set budget alerts and cost allocation tags.".into());
|
|
}
|
|
if !input.contains("reserved") && !input.contains("savings plan") && !input.contains("spot") {
|
|
findings.push("No cost optimization strategy described. Evaluate RIs, savings plans, or spot instances.".into());
|
|
}
|
|
if !input.contains("rightsiz") && !input.contains("right-siz") {
|
|
findings.push(
|
|
"No right-sizing mentioned. Regularly review instance utilization and downsize.".into(),
|
|
);
|
|
}
|
|
findings
|
|
}
|
|
|
|
fn review_pillar_operations(input: &str, _cloud: &str) -> Vec<String> {
|
|
let mut findings = Vec::new();
|
|
if !input.contains("iac")
|
|
&& !input.contains("terraform")
|
|
&& !input.contains("infrastructure as code")
|
|
{
|
|
findings.push(
|
|
"No IaC mentioned. Manage all infrastructure as code for reproducibility.".into(),
|
|
);
|
|
}
|
|
if !input.contains("ci") && !input.contains("pipeline") && !input.contains("deploy") {
|
|
findings.push("No CI/CD described. Automate build, test, and deployment pipelines.".into());
|
|
}
|
|
if !input.contains("runbook") && !input.contains("incident") {
|
|
findings.push(
|
|
"No incident response described. Create runbooks and incident procedures.".into(),
|
|
);
|
|
}
|
|
findings
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn test_config() -> CloudOpsConfig {
|
|
CloudOpsConfig::default()
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn review_iac_detects_security_findings() {
|
|
let tool = CloudOpsTool::new(test_config());
|
|
let result = tool
|
|
.execute(json!({
|
|
"action": "review_iac",
|
|
"input": "resource \"aws_security_group\" \"open\" { ingress { cidr_blocks = [\"0.0.0.0/0\"] } }"
|
|
}))
|
|
.await
|
|
.unwrap();
|
|
|
|
assert!(result.success);
|
|
assert!(result.output.contains("Unrestricted ingress"));
|
|
assert!(result.output.contains("high"));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn review_iac_detects_terraform_type() {
|
|
let tool = CloudOpsTool::new(test_config());
|
|
let result = tool
|
|
.execute(json!({
|
|
"action": "review_iac",
|
|
"input": "resource \"aws_instance\" \"test\" { instance_type = \"t3.micro\" tags = { Name = \"test\" } }"
|
|
}))
|
|
.await
|
|
.unwrap();
|
|
|
|
assert!(result.success);
|
|
assert!(result.output.contains("\"iac_type\": \"terraform\""));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn review_iac_detects_encrypted_false() {
|
|
let tool = CloudOpsTool::new(test_config());
|
|
let result = tool
|
|
.execute(json!({
|
|
"action": "review_iac",
|
|
"input": "resource \"aws_ebs_volume\" \"vol\" { encrypted = false tags = {} }"
|
|
}))
|
|
.await
|
|
.unwrap();
|
|
|
|
assert!(result.success);
|
|
assert!(result.output.contains("Encryption explicitly disabled"));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn cost_analysis_detects_on_demand() {
|
|
let tool = CloudOpsTool::new(test_config());
|
|
let result = tool
|
|
.execute(json!({
|
|
"action": "cost_analysis",
|
|
"input": "service,cost\nEC2 On-Demand,5000\nS3 Storage,200"
|
|
}))
|
|
.await
|
|
.unwrap();
|
|
|
|
assert!(result.success);
|
|
assert!(result.output.contains("on-demand"));
|
|
assert!(result.output.contains("storage"));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn architecture_review_returns_all_pillars() {
|
|
let tool = CloudOpsTool::new(test_config());
|
|
let result = tool
|
|
.execute(json!({
|
|
"action": "architecture_review",
|
|
"input": "Web app with EC2, RDS, S3. No caching layer."
|
|
}))
|
|
.await
|
|
.unwrap();
|
|
|
|
assert!(result.success);
|
|
assert!(result.output.contains("security"));
|
|
assert!(result.output.contains("reliability"));
|
|
assert!(result.output.contains("performance"));
|
|
assert!(result.output.contains("cost_optimization"));
|
|
assert!(result.output.contains("operational_excellence"));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn assess_migration_detects_monolith() {
|
|
let tool = CloudOpsTool::new(test_config());
|
|
let result = tool
|
|
.execute(json!({
|
|
"action": "assess_migration",
|
|
"input": "Legacy monolith application running on VMs with on-premises database."
|
|
}))
|
|
.await
|
|
.unwrap();
|
|
|
|
assert!(result.success);
|
|
assert!(result.output.contains("monolith"));
|
|
assert!(result.output.contains("microservices"));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn empty_input_returns_error() {
|
|
let tool = CloudOpsTool::new(test_config());
|
|
let result = tool
|
|
.execute(json!({
|
|
"action": "review_iac",
|
|
"input": ""
|
|
}))
|
|
.await
|
|
.unwrap();
|
|
|
|
assert!(!result.success);
|
|
assert!(result.error.is_some());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn unsupported_cloud_returns_error() {
|
|
let tool = CloudOpsTool::new(test_config());
|
|
let result = tool
|
|
.execute(json!({
|
|
"action": "review_iac",
|
|
"input": "some content",
|
|
"cloud": "alibaba"
|
|
}))
|
|
.await
|
|
.unwrap();
|
|
|
|
assert!(!result.success);
|
|
assert!(result.error.unwrap().contains("not in supported_clouds"));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn unknown_action_returns_error() {
|
|
let tool = CloudOpsTool::new(test_config());
|
|
let result = tool
|
|
.execute(json!({
|
|
"action": "deploy_everything",
|
|
"input": "some content"
|
|
}))
|
|
.await
|
|
.unwrap();
|
|
|
|
assert!(!result.success);
|
|
assert!(result.error.unwrap().contains("Unknown action"));
|
|
}
|
|
|
|
#[test]
|
|
fn detect_iac_type_identifies_cloudformation() {
|
|
assert_eq!(detect_iac_type("AWS::EC2::Instance"), "cloudformation");
|
|
}
|
|
|
|
#[test]
|
|
fn detect_iac_type_identifies_pulumi() {
|
|
assert_eq!(detect_iac_type("import pulumi"), "pulumi");
|
|
}
|
|
|
|
#[test]
|
|
fn scan_iac_security_finds_wildcard_permission() {
|
|
let findings = scan_iac_security("Action: \"*\" Effect: Allow");
|
|
assert!(!findings.is_empty());
|
|
let msg = findings[0]["message"].as_str().unwrap();
|
|
assert!(msg.contains("Wildcard permission"));
|
|
}
|
|
|
|
#[test]
|
|
fn scan_iac_cost_gates_aws_patterns_for_non_aws() {
|
|
// NAT Gateway / Elastic IP / Load Balancer are AWS-only; should not appear for azure
|
|
let findings = scan_iac_cost(
|
|
"nat_gateway elastic_ip load_balancer instance_type",
|
|
"azure",
|
|
0.0, // threshold 0 so all cost-eligible items pass
|
|
);
|
|
for f in &findings {
|
|
let msg = f["message"].as_str().unwrap();
|
|
assert!(
|
|
!msg.contains("NAT Gateway") && !msg.contains("Elastic IP") && !msg.contains("ALB"),
|
|
"AWS-specific finding leaked for azure: {}",
|
|
msg
|
|
);
|
|
}
|
|
// instance_type is cloud-agnostic and should still appear
|
|
assert!(findings
|
|
.iter()
|
|
.any(|f| f["message"].as_str().unwrap().contains("instance sizing")));
|
|
}
|
|
|
|
#[test]
|
|
fn scan_iac_cost_respects_threshold() {
|
|
// With a high threshold, low-cost patterns should be filtered out
|
|
let findings = scan_iac_cost(
|
|
"nat_gateway elastic_ip instance_type",
|
|
"aws",
|
|
200.0, // above all estimated costs
|
|
);
|
|
assert!(
|
|
findings.is_empty(),
|
|
"expected no findings above threshold 200, got {:?}",
|
|
findings
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn non_string_action_returns_error() {
|
|
let tool = CloudOpsTool::new(test_config());
|
|
let result = tool
|
|
.execute(json!({
|
|
"action": 42,
|
|
"input": "some content"
|
|
}))
|
|
.await;
|
|
|
|
assert!(result.is_err());
|
|
let err_msg = result.unwrap_err().to_string();
|
|
assert!(err_msg.contains("'action' must be a string"));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn non_string_input_returns_error() {
|
|
let tool = CloudOpsTool::new(test_config());
|
|
let result = tool
|
|
.execute(json!({
|
|
"action": "review_iac",
|
|
"input": 123
|
|
}))
|
|
.await;
|
|
|
|
assert!(result.is_err());
|
|
let err_msg = result.unwrap_err().to_string();
|
|
assert!(err_msg.contains("'input' must be a string"));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn non_string_cloud_returns_error() {
|
|
let tool = CloudOpsTool::new(test_config());
|
|
let result = tool
|
|
.execute(json!({
|
|
"action": "review_iac",
|
|
"input": "some content",
|
|
"cloud": true
|
|
}))
|
|
.await;
|
|
|
|
assert!(result.is_err());
|
|
let err_msg = result.unwrap_err().to_string();
|
|
assert!(err_msg.contains("'cloud' must be a string"));
|
|
}
|
|
}
|