Skip to content

Commit 428a707

Browse files
api-clients-generation-pipeline[bot]ci.datadog-api-spec
andauthored
Security Monitoring - Validation Endpoint for Suppressions (#887)
Co-authored-by: ci.datadog-api-spec <[email protected]>
1 parent 41af9f2 commit 428a707

10 files changed

+341
-0
lines changed

.generator/schemas/v2/openapi.yaml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64816,6 +64816,38 @@ paths:
6481664816
summary: Get suppressions affecting a specific rule
6481764817
tags:
6481864818
- Security Monitoring
64819+
/api/v2/security_monitoring/configuration/suppressions/validation:
64820+
post:
64821+
description: Validate a suppression rule.
64822+
operationId: ValidateSecurityMonitoringSuppression
64823+
requestBody:
64824+
content:
64825+
application/json:
64826+
schema:
64827+
$ref: '#/components/schemas/SecurityMonitoringSuppressionUpdateRequest'
64828+
required: true
64829+
responses:
64830+
'204':
64831+
description: OK
64832+
'400':
64833+
$ref: '#/components/responses/BadRequestResponse'
64834+
'403':
64835+
$ref: '#/components/responses/NotAuthorizedResponse'
64836+
'429':
64837+
$ref: '#/components/responses/TooManyRequestsResponse'
64838+
security:
64839+
- apiKeyAuth: []
64840+
appKeyAuth: []
64841+
- AuthZ:
64842+
- security_monitoring_suppressions_write
64843+
summary: Validate a suppression rule
64844+
tags:
64845+
- Security Monitoring
64846+
x-codegen-request-body-name: body
64847+
x-permission:
64848+
operator: OR
64849+
permissions:
64850+
- security_monitoring_suppressions_write
6481964851
/api/v2/security_monitoring/configuration/suppressions/{suppression_id}:
6482064852
delete:
6482164853
description: Delete a specific suppression rule.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Validate a suppression rule returns "OK" response
2+
use datadog_api_client::datadog;
3+
use datadog_api_client::datadogV2::api_security_monitoring::SecurityMonitoringAPI;
4+
use datadog_api_client::datadogV2::model::SecurityMonitoringSuppressionType;
5+
use datadog_api_client::datadogV2::model::SecurityMonitoringSuppressionUpdateAttributes;
6+
use datadog_api_client::datadogV2::model::SecurityMonitoringSuppressionUpdateData;
7+
use datadog_api_client::datadogV2::model::SecurityMonitoringSuppressionUpdateRequest;
8+
9+
#[tokio::main]
10+
async fn main() {
11+
let body = SecurityMonitoringSuppressionUpdateRequest::new(
12+
SecurityMonitoringSuppressionUpdateData::new(
13+
SecurityMonitoringSuppressionUpdateAttributes::new()
14+
.data_exclusion_query("source:cloudtrail account_id:12345".to_string())
15+
.description(
16+
"This rule suppresses low-severity signals in staging environments."
17+
.to_string(),
18+
)
19+
.enabled(true)
20+
.name("Custom suppression".to_string())
21+
.rule_query("type:log_detection source:cloudtrail".to_string()),
22+
SecurityMonitoringSuppressionType::SUPPRESSIONS,
23+
),
24+
);
25+
let configuration = datadog::Configuration::new();
26+
let api = SecurityMonitoringAPI::with_config(configuration);
27+
let resp = api.validate_security_monitoring_suppression(body).await;
28+
if let Ok(value) = resp {
29+
println!("{:#?}", value);
30+
} else {
31+
println!("{:#?}", resp.unwrap_err());
32+
}
33+
}

src/datadogV2/api/api_security_monitoring.rs

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1324,6 +1324,14 @@ pub enum ValidateSecurityMonitoringRuleError {
13241324
UnknownValue(serde_json::Value),
13251325
}
13261326

1327+
/// ValidateSecurityMonitoringSuppressionError is a struct for typed errors of method [`SecurityMonitoringAPI::validate_security_monitoring_suppression`]
1328+
#[derive(Debug, Clone, Serialize, Deserialize)]
1329+
#[serde(untagged)]
1330+
pub enum ValidateSecurityMonitoringSuppressionError {
1331+
APIErrorResponse(crate::datadogV2::model::APIErrorResponse),
1332+
UnknownValue(serde_json::Value),
1333+
}
1334+
13271335
/// Create and manage your security rules, signals, filters, and more. See the [Datadog Security page](<https://docs.datadoghq.com/security/>) for more information.
13281336
#[derive(Debug, Clone)]
13291337
pub struct SecurityMonitoringAPI {
@@ -9975,4 +9983,143 @@ impl SecurityMonitoringAPI {
99759983
Err(datadog::Error::ResponseError(local_error))
99769984
}
99779985
}
9986+
9987+
/// Validate a suppression rule.
9988+
pub async fn validate_security_monitoring_suppression(
9989+
&self,
9990+
body: crate::datadogV2::model::SecurityMonitoringSuppressionUpdateRequest,
9991+
) -> Result<(), datadog::Error<ValidateSecurityMonitoringSuppressionError>> {
9992+
match self
9993+
.validate_security_monitoring_suppression_with_http_info(body)
9994+
.await
9995+
{
9996+
Ok(_) => Ok(()),
9997+
Err(err) => Err(err),
9998+
}
9999+
}
10000+
10001+
/// Validate a suppression rule.
10002+
pub async fn validate_security_monitoring_suppression_with_http_info(
10003+
&self,
10004+
body: crate::datadogV2::model::SecurityMonitoringSuppressionUpdateRequest,
10005+
) -> Result<
10006+
datadog::ResponseContent<()>,
10007+
datadog::Error<ValidateSecurityMonitoringSuppressionError>,
10008+
> {
10009+
let local_configuration = &self.config;
10010+
let operation_id = "v2.validate_security_monitoring_suppression";
10011+
10012+
let local_client = &self.client;
10013+
10014+
let local_uri_str = format!(
10015+
"{}/api/v2/security_monitoring/configuration/suppressions/validation",
10016+
local_configuration.get_operation_host(operation_id)
10017+
);
10018+
let mut local_req_builder =
10019+
local_client.request(reqwest::Method::POST, local_uri_str.as_str());
10020+
10021+
// build headers
10022+
let mut headers = HeaderMap::new();
10023+
headers.insert("Content-Type", HeaderValue::from_static("application/json"));
10024+
headers.insert("Accept", HeaderValue::from_static("*/*"));
10025+
10026+
// build user agent
10027+
match HeaderValue::from_str(local_configuration.user_agent.as_str()) {
10028+
Ok(user_agent) => headers.insert(reqwest::header::USER_AGENT, user_agent),
10029+
Err(e) => {
10030+
log::warn!("Failed to parse user agent header: {e}, falling back to default");
10031+
headers.insert(
10032+
reqwest::header::USER_AGENT,
10033+
HeaderValue::from_static(datadog::DEFAULT_USER_AGENT.as_str()),
10034+
)
10035+
}
10036+
};
10037+
10038+
// build auth
10039+
if let Some(local_key) = local_configuration.auth_keys.get("apiKeyAuth") {
10040+
headers.insert(
10041+
"DD-API-KEY",
10042+
HeaderValue::from_str(local_key.key.as_str())
10043+
.expect("failed to parse DD-API-KEY header"),
10044+
);
10045+
};
10046+
if let Some(local_key) = local_configuration.auth_keys.get("appKeyAuth") {
10047+
headers.insert(
10048+
"DD-APPLICATION-KEY",
10049+
HeaderValue::from_str(local_key.key.as_str())
10050+
.expect("failed to parse DD-APPLICATION-KEY header"),
10051+
);
10052+
};
10053+
10054+
// build body parameters
10055+
let output = Vec::new();
10056+
let mut ser = serde_json::Serializer::with_formatter(output, datadog::DDFormatter);
10057+
if body.serialize(&mut ser).is_ok() {
10058+
if let Some(content_encoding) = headers.get("Content-Encoding") {
10059+
match content_encoding.to_str().unwrap_or_default() {
10060+
"gzip" => {
10061+
let mut enc = GzEncoder::new(Vec::new(), Compression::default());
10062+
let _ = enc.write_all(ser.into_inner().as_slice());
10063+
match enc.finish() {
10064+
Ok(buf) => {
10065+
local_req_builder = local_req_builder.body(buf);
10066+
}
10067+
Err(e) => return Err(datadog::Error::Io(e)),
10068+
}
10069+
}
10070+
"deflate" => {
10071+
let mut enc = ZlibEncoder::new(Vec::new(), Compression::default());
10072+
let _ = enc.write_all(ser.into_inner().as_slice());
10073+
match enc.finish() {
10074+
Ok(buf) => {
10075+
local_req_builder = local_req_builder.body(buf);
10076+
}
10077+
Err(e) => return Err(datadog::Error::Io(e)),
10078+
}
10079+
}
10080+
"zstd1" => {
10081+
let mut enc = zstd::stream::Encoder::new(Vec::new(), 0).unwrap();
10082+
let _ = enc.write_all(ser.into_inner().as_slice());
10083+
match enc.finish() {
10084+
Ok(buf) => {
10085+
local_req_builder = local_req_builder.body(buf);
10086+
}
10087+
Err(e) => return Err(datadog::Error::Io(e)),
10088+
}
10089+
}
10090+
_ => {
10091+
local_req_builder = local_req_builder.body(ser.into_inner());
10092+
}
10093+
}
10094+
} else {
10095+
local_req_builder = local_req_builder.body(ser.into_inner());
10096+
}
10097+
}
10098+
10099+
local_req_builder = local_req_builder.headers(headers);
10100+
let local_req = local_req_builder.build()?;
10101+
log::debug!("request content: {:?}", local_req.body());
10102+
let local_resp = local_client.execute(local_req).await?;
10103+
10104+
let local_status = local_resp.status();
10105+
let local_content = local_resp.text().await?;
10106+
log::debug!("response content: {}", local_content);
10107+
10108+
if !local_status.is_client_error() && !local_status.is_server_error() {
10109+
Ok(datadog::ResponseContent {
10110+
status: local_status,
10111+
content: local_content,
10112+
entity: None,
10113+
})
10114+
} else {
10115+
let local_entity: Option<ValidateSecurityMonitoringSuppressionError> =
10116+
serde_json::from_str(&local_content).ok();
10117+
let local_error = datadog::ResponseContent {
10118+
status: local_status,
10119+
content: local_content,
10120+
entity: local_entity,
10121+
};
10122+
Err(datadog::Error::ResponseError(local_error))
10123+
}
10124+
}
997810125
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
2025-09-01T21:36:42.334Z
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
{
2+
"http_interactions": [
3+
{
4+
"request": {
5+
"body": {
6+
"string": "{\"data\":{\"attributes\":{\"data_exclusion_query\":\"not enough attributes\"},\"type\":\"suppressions\"}}",
7+
"encoding": null
8+
},
9+
"headers": {
10+
"Accept": [
11+
"*/*"
12+
],
13+
"Content-Type": [
14+
"application/json"
15+
]
16+
},
17+
"method": "post",
18+
"uri": "https://api.datadoghq.com/api/v2/security_monitoring/configuration/suppressions/validation"
19+
},
20+
"response": {
21+
"body": {
22+
"string": "{\"errors\":[\"input_validation_error(Field 'data.attributes.rule_query' is invalid: field 'rule_query' is required)\",\"input_validation_error(Field 'data.attributes.name' is invalid: name cannot be empty)\"]}",
23+
"encoding": null
24+
},
25+
"headers": {
26+
"Content-Type": [
27+
"application/json"
28+
]
29+
},
30+
"status": {
31+
"code": 400,
32+
"message": "Bad Request"
33+
}
34+
},
35+
"recorded_at": "Mon, 01 Sep 2025 21:36:42 GMT"
36+
}
37+
],
38+
"recorded_with": "VCR 6.0.0"
39+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
2025-09-01T21:36:20.593Z
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
{
2+
"http_interactions": [
3+
{
4+
"request": {
5+
"body": {
6+
"string": "{\"data\":{\"attributes\":{\"data_exclusion_query\":\"source:cloudtrail account_id:12345\",\"description\":\"This rule suppresses low-severity signals in staging environments.\",\"enabled\":true,\"name\":\"Custom suppression\",\"rule_query\":\"type:log_detection source:cloudtrail\"},\"type\":\"suppressions\"}}",
7+
"encoding": null
8+
},
9+
"headers": {
10+
"Accept": [
11+
"*/*"
12+
],
13+
"Content-Type": [
14+
"application/json"
15+
]
16+
},
17+
"method": "post",
18+
"uri": "https://api.datadoghq.com/api/v2/security_monitoring/configuration/suppressions/validation"
19+
},
20+
"response": {
21+
"body": {
22+
"string": "",
23+
"encoding": null
24+
},
25+
"headers": {},
26+
"status": {
27+
"code": 204,
28+
"message": "No Content"
29+
}
30+
},
31+
"recorded_at": "Mon, 01 Sep 2025 21:36:20 GMT"
32+
}
33+
],
34+
"recorded_with": "VCR 6.0.0"
35+
}

tests/scenarios/features/v2/security_monitoring.feature

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1389,3 +1389,17 @@ Feature: Security Monitoring
13891389
And body with value {"cases":[{"name":"","status":"info","notifications":[],"condition":"a > 0"}],"hasExtendedTitle":true,"isEnabled":true,"message":"My security monitoring rule","name":"My security monitoring rule","options":{"evaluationWindow":1800,"keepAlive":1800,"maxSignalDuration":1800,"detectionMethod":"threshold"},"queries":[{"query":"source:source_here","groupByFields":["@userIdentity.assumed_role"],"distinctFields":[],"aggregation":"count","name":""}],"tags":["env:prod","team:security"],"type":"log_detection"}
13901390
When the request is sent
13911391
Then the response status is 204 OK
1392+
1393+
@team:DataDog/k9-cloud-security-platform
1394+
Scenario: Validate a suppression rule returns "Bad Request" response
1395+
Given new "ValidateSecurityMonitoringSuppression" request
1396+
And body with value {"data": {"attributes": {"data_exclusion_query": "not enough attributes"}, "type": "suppressions"}}
1397+
When the request is sent
1398+
Then the response status is 400 Bad Request
1399+
1400+
@team:DataDog/k9-cloud-security-platform
1401+
Scenario: Validate a suppression rule returns "OK" response
1402+
Given new "ValidateSecurityMonitoringSuppression" request
1403+
And body with value {"data": {"attributes": {"data_exclusion_query": "source:cloudtrail account_id:12345", "description": "This rule suppresses low-severity signals in staging environments.", "enabled": true, "name": "Custom suppression", "rule_query": "type:log_detection source:cloudtrail"}, "type": "suppressions"}}
1404+
When the request is sent
1405+
Then the response status is 204 OK

tests/scenarios/features/v2/undo.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3024,6 +3024,12 @@
30243024
"type": "safe"
30253025
}
30263026
},
3027+
"ValidateSecurityMonitoringSuppression": {
3028+
"tag": "Security Monitoring",
3029+
"undo": {
3030+
"type": "idempotent"
3031+
}
3032+
},
30273033
"DeleteSecurityMonitoringSuppression": {
30283034
"tag": "Security Monitoring",
30293035
"undo": {

tests/scenarios/function_mappings.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2205,6 +2205,10 @@ pub fn collect_function_calls(world: &mut DatadogWorld) {
22052205
"v2.GetSuppressionsAffectingRule".into(),
22062206
test_v2_get_suppressions_affecting_rule,
22072207
);
2208+
world.function_mappings.insert(
2209+
"v2.ValidateSecurityMonitoringSuppression".into(),
2210+
test_v2_validate_security_monitoring_suppression,
2211+
);
22082212
world.function_mappings.insert(
22092213
"v2.DeleteSecurityMonitoringSuppression".into(),
22102214
test_v2_delete_security_monitoring_suppression,
@@ -15719,6 +15723,35 @@ fn test_v2_get_suppressions_affecting_rule(
1571915723
world.response.code = response.status.as_u16();
1572015724
}
1572115725

15726+
fn test_v2_validate_security_monitoring_suppression(
15727+
world: &mut DatadogWorld,
15728+
_parameters: &HashMap<String, Value>,
15729+
) {
15730+
let api = world
15731+
.api_instances
15732+
.v2_api_security_monitoring
15733+
.as_ref()
15734+
.expect("api instance not found");
15735+
let body = serde_json::from_value(_parameters.get("body").unwrap().clone()).unwrap();
15736+
let response = match block_on(api.validate_security_monitoring_suppression_with_http_info(body))
15737+
{
15738+
Ok(response) => response,
15739+
Err(error) => {
15740+
return match error {
15741+
Error::ResponseError(e) => {
15742+
world.response.code = e.status.as_u16();
15743+
if let Some(entity) = e.entity {
15744+
world.response.object = serde_json::to_value(entity).unwrap();
15745+
}
15746+
}
15747+
_ => panic!("error parsing response: {error}"),
15748+
};
15749+
}
15750+
};
15751+
world.response.object = serde_json::to_value(response.entity).unwrap();
15752+
world.response.code = response.status.as_u16();
15753+
}
15754+
1572215755
fn test_v2_delete_security_monitoring_suppression(
1572315756
world: &mut DatadogWorld,
1572415757
_parameters: &HashMap<String, Value>,

0 commit comments

Comments
 (0)