diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md
index 0948138..fa1172c 100644
--- a/docs/COMMANDS.md
+++ b/docs/COMMANDS.md
@@ -161,6 +161,8 @@ pup infrastructure hosts list
### Security & Compliance
- **security** - Security monitoring (rules, signals, findings, content-packs, risk-scores)
+ - `pup security findings mute --file
` — Mute or unmute up to 100 findings (stable, SDK #1519/#1660)
+ - `pup security rules bulk-convert --file ` — Bulk convert existing rules to Terraform ZIP archive (SDK #1675)
- **static-analysis** - Code security (custom-rulesets, custom-rules)
- **audit-logs** - Audit trail (list, search)
- **data-governance** - Sensitive data scanning (scanner-rules list)
diff --git a/src/auth/types.rs b/src/auth/types.rs
index f86dfcd..168e779 100644
--- a/src/auth/types.rs
+++ b/src/auth/types.rs
@@ -209,6 +209,7 @@ pub fn default_scopes() -> Vec<&'static str> {
"security_monitoring_filters_read",
"security_monitoring_filters_write",
"security_monitoring_findings_read",
+ "security_monitoring_findings_write",
"security_monitoring_rules_read",
"security_monitoring_rules_write",
"security_monitoring_signals_read",
diff --git a/src/commands/security.rs b/src/commands/security.rs
index 3f7baf6..72fa945 100644
--- a/src/commands/security.rs
+++ b/src/commands/security.rs
@@ -18,9 +18,10 @@ use datadog_api_client::datadogV2::api_security_monitoring::{
use datadog_api_client::datadogV2::model::{
ApplicationSecurityWafCustomRuleCreateRequest, ApplicationSecurityWafCustomRuleUpdateRequest,
ApplicationSecurityWafExclusionFilterCreateRequest,
- ApplicationSecurityWafExclusionFilterUpdateRequest, RestrictionPolicyUpdateRequest,
- SecurityMonitoringRuleBulkExportAttributes, SecurityMonitoringRuleBulkExportData,
- SecurityMonitoringRuleBulkExportDataType, SecurityMonitoringRuleBulkExportPayload,
+ ApplicationSecurityWafExclusionFilterUpdateRequest, MuteFindingsRequest,
+ RestrictionPolicyUpdateRequest, SecurityMonitoringRuleBulkExportAttributes,
+ SecurityMonitoringRuleBulkExportData, SecurityMonitoringRuleBulkExportDataType,
+ SecurityMonitoringRuleBulkExportPayload, SecurityMonitoringRuleConvertBulkPayload,
SecurityMonitoringRuleConvertPayload, SecurityMonitoringRuleSort,
SecurityMonitoringSignalListRequest, SecurityMonitoringSignalListRequestFilter,
SecurityMonitoringSignalListRequestPage, SecurityMonitoringSignalsSort,
@@ -288,6 +289,21 @@ pub async fn findings_search(cfg: &Config, query: Option, limit: i64) ->
formatter::output(cfg, &resp)
}
+// ---- Mute Findings ----
+
+/// Mute or unmute security findings (stable, SDK #1519/#1660).
+/// Accepts up to 100 finding IDs per request. The `--file` must contain a
+/// JSON body shaped as `MuteFindingsRequest` (see Datadog docs).
+pub async fn findings_mute(cfg: &Config, file: &str) -> Result<()> {
+ let body: MuteFindingsRequest = util::read_json_file(file)?;
+ let api = crate::make_api!(SecurityMonitoringAPI, cfg);
+ let resp = api
+ .mute_security_findings(body)
+ .await
+ .map_err(|e| anyhow::anyhow!("failed to mute findings: {e:?}"))?;
+ formatter::output(cfg, &resp)
+}
+
// ---- Bulk Export ----
pub async fn rules_bulk_export(cfg: &Config, rule_ids: Vec) -> Result<()> {
@@ -330,6 +346,22 @@ pub async fn rules_to_terraform(cfg: &Config, file: &str) -> Result<()> {
formatter::output(cfg, &resp)
}
+/// Bulk convert existing security monitoring rules to Terraform (SDK #1675).
+/// The `--file` must contain a JSON body shaped as
+/// `SecurityMonitoringRuleConvertBulkPayload`. Returns a ZIP archive written
+/// to stdout (pipe to a file if you want to save it).
+pub async fn rules_bulk_convert(cfg: &Config, file: &str) -> Result<()> {
+ let body: SecurityMonitoringRuleConvertBulkPayload = util::read_json_file(file)?;
+ let api = crate::make_api!(SecurityMonitoringAPI, cfg);
+ let bytes = api
+ .bulk_convert_existing_security_monitoring_rules(body)
+ .await
+ .map_err(|e| anyhow::anyhow!("failed to bulk convert security rules: {e:?}"))?;
+ let output = String::from_utf8_lossy(&bytes);
+ println!("{output}");
+ Ok(())
+}
+
pub async fn terraform_export(cfg: &Config, resource_type: &str, resource_id: &str) -> Result<()> {
let rt = parse_terraform_resource_type(resource_type)?;
let api = crate::make_api!(SecurityMonitoringAPI, cfg);
@@ -1099,4 +1131,104 @@ mod tests {
.to_string()
.contains("invalid --sort value"));
}
+
+ #[tokio::test]
+ async fn test_findings_mute_ok() {
+ let _lock = lock_env().await;
+ std::env::set_var("DD_TOKEN_STORAGE", "file");
+ let mut server = mockito::Server::new_async().await;
+ let cfg = test_config(&server.url());
+ let tmp = write_temp_json(
+ "pup_test_findings_mute.json",
+ r#"{"data":{"type":"mute","attributes":{"mute":{"is_muted":true,"reason":"FALSE_POSITIVE"}},"relationships":{"findings":{"data":[]}}}}"#,
+ );
+ let _mock = mock_any(
+ &mut server,
+ "PATCH",
+ r#"{"data":{"id":"mute-job-1","type":"mute_findings_response"}}"#,
+ )
+ .await;
+ let result = super::findings_mute(&cfg, tmp.to_str().unwrap()).await;
+ assert!(result.is_ok(), "findings_mute failed: {:?}", result.err());
+ let _ = std::fs::remove_file(tmp);
+ cleanup_env();
+ std::env::remove_var("DD_TOKEN_STORAGE");
+ }
+
+ #[tokio::test]
+ async fn test_findings_mute_error() {
+ let _lock = lock_env().await;
+ std::env::set_var("DD_TOKEN_STORAGE", "file");
+ let mut server = mockito::Server::new_async().await;
+ let cfg = test_config(&server.url());
+ let tmp = write_temp_json(
+ "pup_test_findings_mute_err.json",
+ r#"{"data":{"type":"mute","attributes":{"mute":{"is_muted":true,"reason":"FALSE_POSITIVE"}},"relationships":{"findings":{"data":[]}}}}"#,
+ );
+ let _mock = server
+ .mock("PATCH", mockito::Matcher::Any)
+ .with_status(403)
+ .with_header("content-type", "application/json")
+ .with_body(r#"{"errors":["Forbidden"]}"#)
+ .create_async()
+ .await;
+ let result = super::findings_mute(&cfg, tmp.to_str().unwrap()).await;
+ assert!(result.is_err(), "expected error for 403 response");
+ let _ = std::fs::remove_file(tmp);
+ cleanup_env();
+ std::env::remove_var("DD_TOKEN_STORAGE");
+ }
+
+ #[tokio::test]
+ async fn test_rules_bulk_convert_ok() {
+ let _lock = lock_env().await;
+ std::env::set_var("DD_TOKEN_STORAGE", "file");
+ let mut server = mockito::Server::new_async().await;
+ let cfg = test_config(&server.url());
+ let tmp = write_temp_json(
+ "pup_test_rules_bulk_convert.json",
+ r#"{"data":{"type":"security_monitoring_rules_convert_bulk","attributes":{"ruleIds":["abc-123"]}}}"#,
+ );
+ let zip_bytes: &[u8] = b"PK\x03\x04fake-zip-bytes";
+ let _mock = server
+ .mock("POST", mockito::Matcher::Any)
+ .with_status(200)
+ .with_header("content-type", "application/zip")
+ .with_body(zip_bytes)
+ .create_async()
+ .await;
+ let result = super::rules_bulk_convert(&cfg, tmp.to_str().unwrap()).await;
+ assert!(
+ result.is_ok(),
+ "rules_bulk_convert failed: {:?}",
+ result.err()
+ );
+ let _ = std::fs::remove_file(tmp);
+ cleanup_env();
+ std::env::remove_var("DD_TOKEN_STORAGE");
+ }
+
+ #[tokio::test]
+ async fn test_rules_bulk_convert_error() {
+ let _lock = lock_env().await;
+ std::env::set_var("DD_TOKEN_STORAGE", "file");
+ let mut server = mockito::Server::new_async().await;
+ let cfg = test_config(&server.url());
+ let tmp = write_temp_json(
+ "pup_test_rules_bulk_convert_err.json",
+ r#"{"data":{"type":"security_monitoring_rules_convert_bulk","attributes":{"ruleIds":["bad"]}}}"#,
+ );
+ let _mock = server
+ .mock("POST", mockito::Matcher::Any)
+ .with_status(400)
+ .with_header("content-type", "application/json")
+ .with_body(r#"{"errors":["Bad Request"]}"#)
+ .create_async()
+ .await;
+ let result = super::rules_bulk_convert(&cfg, tmp.to_str().unwrap()).await;
+ assert!(result.is_err(), "expected error for 400 response");
+ let _ = std::fs::remove_file(tmp);
+ cleanup_env();
+ std::env::remove_var("DD_TOKEN_STORAGE");
+ }
}
diff --git a/src/main.rs b/src/main.rs
index 8fbdede..151bf11 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -4689,6 +4689,15 @@ enum SecurityRuleActions {
#[arg(long, help = "JSON file with the rule conversion payload (required)")]
file: String,
},
+ /// Bulk convert existing rules to Terraform (returns a ZIP archive)
+ #[command(name = "bulk-convert")]
+ BulkConvert {
+ #[arg(
+ long,
+ help = "JSON file with SecurityMonitoringRuleConvertBulkPayload body (required)"
+ )]
+ file: String,
+ },
}
#[derive(Subcommand)]
@@ -4759,6 +4768,11 @@ enum SecurityFindingActions {
#[arg(long, default_value_t = 100)]
limit: i64,
},
+ /// Mute or unmute security findings (up to 100 per request)
+ Mute {
+ #[arg(long, help = "JSON file with MuteFindingsRequest body (required)")]
+ file: String,
+ },
}
#[derive(Subcommand)]
@@ -11674,6 +11688,9 @@ async fn main_inner() -> anyhow::Result<()> {
SecurityRuleActions::ToTerraform { file } => {
commands::security::rules_to_terraform(&cfg, &file).await?;
}
+ SecurityRuleActions::BulkConvert { file } => {
+ commands::security::rules_bulk_convert(&cfg, &file).await?;
+ }
},
SecurityActions::Signals { action } => match action {
SecuritySignalActions::List {
@@ -11709,6 +11726,9 @@ async fn main_inner() -> anyhow::Result<()> {
commands::security::findings_analyze(&cfg, &query, &from, &to, limit)
.await?;
}
+ SecurityFindingActions::Mute { file } => {
+ commands::security::findings_mute(&cfg, &file).await?;
+ }
},
SecurityActions::ContentPacks { action } => match action {
SecurityContentPackActions::List => {