Skip to content

Commit 8cc30d2

Browse files
[DSEC-153] add check end-to-end test (#54323)
<!--Please give us some feedback on your experience writing this PR ! https://app.datadoghq.com/forms/43db4c02-6837-400c-8083-692e141b1b88 !--> ### What does this PR do? Adds an end-to-end test for the `datasecurity` check: it drives a full check run through a test-only `mock` scan engine and asserts the emitted `sds-result` payload (matches, scanned columns, row count, status). ### Motivation The scan path (`run_scan` / result building) had no test coverage (DSEC-153). ### Describe how you validated your changes `cargo test` in the `datasecurity` crate. ### Additional Notes Test-only: the `mock` engine (`platform: mock`) is compiled under `#[cfg(test)]` and returns caller-controlled `ScanData`; no production behavior changes. Co-authored-by: aimene.belfodil <aimene.belfodil@datadoghq.com>
1 parent ca8939a commit 8cc30d2

3 files changed

Lines changed: 171 additions & 2 deletions

File tree

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
//! Test-only scan engine (`platform: mock`) returning caller-controlled data.
2+
3+
use std::cell::RefCell;
4+
5+
use anyhow::Result;
6+
7+
use super::{ScanData, ScanEngine};
8+
use crate::config::SubTask;
9+
10+
thread_local! {
11+
static DATA: RefCell<ScanData> = RefCell::new(ScanData::default());
12+
}
13+
14+
/// Sets the [`ScanData`] the mock engine returns from `fetch_data`.
15+
pub(crate) fn set_data(data: ScanData) {
16+
DATA.with(|d| *d.borrow_mut() = data);
17+
}
18+
19+
pub(super) struct MockEngine;
20+
pub(super) const ENGINE: MockEngine = MockEngine;
21+
22+
impl ScanEngine for MockEngine {
23+
fn name(&self) -> &'static str {
24+
"mock"
25+
}
26+
27+
fn fetch_data(&self, _sub_task: &SubTask) -> Result<ScanData> {
28+
Ok(DATA.with(|d| d.borrow().clone()))
29+
}
30+
}

pkg/collector/sharedlibrary/rustchecks/checks/datasecurity/src/backend/mod.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ use crate::config::SubTask;
88
#[cfg(feature = "engine-postgres")]
99
mod postgres;
1010

11+
#[cfg(test)]
12+
pub(crate) mod mock;
13+
1114
/// One scanned column's name and its source data type (e.g. `text`, `varchar`).
1215
#[derive(Debug, Default, Clone, PartialEq, Eq)]
1316
pub struct ScannedColumn {
@@ -17,7 +20,7 @@ pub struct ScannedColumn {
1720

1821
/// The result of running a sub task's query: the `{ column: [values] }` map fed
1922
/// to the scanner, plus metadata describing what was scanned.
20-
#[derive(Debug, Default)]
23+
#[derive(Debug, Default, Clone)]
2124
pub struct ScanData {
2225
/// Column-oriented values consumed by the scanner.
2326
// TODO(dsec-173): return an `Event` (dd-sensitive-data-scanner) per backend
@@ -43,6 +46,8 @@ fn engines() -> &'static [&'static dyn ScanEngine] {
4346
&[
4447
#[cfg(feature = "engine-postgres")]
4548
&postgres::ENGINE,
49+
#[cfg(test)]
50+
&mock::ENGINE,
4651
]
4752
}
4853

pkg/collector/sharedlibrary/rustchecks/checks/datasecurity/src/check.rs

Lines changed: 135 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,6 @@ fn run_sub_task(
9191

9292
/// Fetches the sub task's data and scans it, returning the matches and the
9393
/// scanned-table statistics.
94-
/// TODO(dsec-161): add tests for the scan.
9594
fn run_scan(scanner: &Scanner, sub_task: &SubTask) -> Result<ScanOutcome> {
9695
let data = backend::fetch_data(sub_task).context("fetching sub task data")?;
9796
let matches = scanner
@@ -103,3 +102,138 @@ fn run_scan(scanner: &Scanner, sub_task: &SubTask) -> Result<ScanOutcome> {
103102
scanned_row_count: data.scanned_row_count,
104103
})
105104
}
105+
106+
#[cfg(test)]
107+
mod tests {
108+
use prost::Message;
109+
use serde_json::json;
110+
use shlib_core::stubs::AggregatorStub;
111+
112+
use crate::backend::{ScanData, ScannedColumn, mock};
113+
use crate::constants::SDS_RESULT_EVENT_TYPE;
114+
use crate::proto::{
115+
PostgresScannedColumn, PostgresTable, Resource, ScanLocation, ScanMetadata, ScanResult,
116+
ScanTaskMetadata, ScanningSource, SdsResultPayload, Status, TableMatch, scan_location,
117+
scanning_source,
118+
};
119+
120+
use super::check;
121+
122+
const INSTANCE: &str = r#"
123+
task_id: task-1
124+
scanning_rules:
125+
- id: email
126+
pattern: '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]+'
127+
scan_data:
128+
- sub_task_id: sub-1
129+
query: SELECT email FROM users
130+
timeout_seconds: 5
131+
connection:
132+
host: mock
133+
dbname: app
134+
entity:
135+
platform: mock
136+
database_cluster_name: cluster
137+
database_instance_name: inst
138+
database: app
139+
schema: public
140+
table: users
141+
"#;
142+
143+
#[test]
144+
fn scans_data_and_emits_sds_result() {
145+
// The mock engine returns this in place of a real query: two scanned
146+
// columns, `email` (which matches) and `name` (which does not).
147+
mock::set_data(ScanData {
148+
columns: json!({
149+
"email": ["alice@corp.io", "bob@corp.io hatem@corp.io"],
150+
"name": ["alice", "bob"],
151+
}),
152+
scanned_columns: vec![
153+
ScannedColumn {
154+
name: "email".to_string(),
155+
data_type: "text".to_string(),
156+
},
157+
ScannedColumn {
158+
name: "name".to_string(),
159+
data_type: "varchar".to_string(),
160+
},
161+
],
162+
scanned_row_count: 2,
163+
});
164+
165+
let aggregator = AggregatorStub::new();
166+
check(&aggregator.agent_check("{}", INSTANCE)).expect("check run failed");
167+
168+
// Exactly one sds-result event platform event is emitted.
169+
let events = aggregator.event_platform_events();
170+
assert_eq!(events.len(), 1);
171+
assert_eq!(events[0].event_type, SDS_RESULT_EVENT_TYPE);
172+
173+
// The decoded payload matches in full (timestamp aside, it is clock-based).
174+
let payload =
175+
SdsResultPayload::decode(events[0].raw_event.as_slice()).expect("payload decodes");
176+
assert!(payload.timestamp > 0, "timestamp should be populated");
177+
178+
assert_eq!(
179+
payload,
180+
SdsResultPayload {
181+
timestamp: payload.timestamp,
182+
resource: Some(Resource {
183+
r#type: "postgres_table".to_string(),
184+
name: "inst.app.public.users".to_string(),
185+
}),
186+
rule_ids: vec!["email".to_string()],
187+
scanning_source: Some(ScanningSource {
188+
source: Some(scanning_source::Source::Agent(
189+
scanning_source::Agent::default()
190+
)),
191+
}),
192+
scan_results: vec![ScanResult {
193+
table_matches: vec![TableMatch {
194+
rule_id: "email".to_string(),
195+
column_name: "email".to_string(),
196+
count_matched_rows: 2,
197+
count_matches: 3,
198+
..Default::default()
199+
}],
200+
location: Some(ScanLocation {
201+
scan_location: Some(scan_location::ScanLocation::PostgresTable(
202+
PostgresTable {
203+
database_cluster_name: "cluster".to_string(),
204+
database_instance_name: "inst".to_string(),
205+
database_host_name: "mock".to_string(),
206+
database_name: "app".to_string(),
207+
schema_name: "public".to_string(),
208+
table_name: "users".to_string(),
209+
scanned_row_count: 2,
210+
scanned_columns: vec![
211+
PostgresScannedColumn {
212+
name: "email".to_string(),
213+
data_type: "text".to_string(),
214+
},
215+
PostgresScannedColumn {
216+
name: "name".to_string(),
217+
data_type: "varchar".to_string(),
218+
},
219+
],
220+
..Default::default()
221+
}
222+
)),
223+
..Default::default()
224+
}),
225+
scan_metadata: Some(ScanMetadata {
226+
scan_task_metadata: Some(ScanTaskMetadata {
227+
task_id: "task-1".to_string(),
228+
sub_task_id: "sub-1".to_string(),
229+
status: Status::Success as i32,
230+
..Default::default()
231+
}),
232+
}),
233+
..Default::default()
234+
}],
235+
..Default::default()
236+
}
237+
);
238+
}
239+
}

0 commit comments

Comments
 (0)