Skip to content
Merged
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
2 changes: 2 additions & 0 deletions docs/COMMANDS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <body.json>` — Mute or unmute up to 100 findings (stable, SDK #1519/#1660)
- `pup security rules bulk-convert --file <payload.json>` — 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)
Expand Down
1 change: 1 addition & 0 deletions src/auth/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
138 changes: 135 additions & 3 deletions src/commands/security.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -288,6 +289,21 @@ pub async fn findings_search(cfg: &Config, query: Option<String>, 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<String>) -> Result<()> {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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");
}
}
20 changes: 20 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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)]
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 => {
Expand Down
Loading