Skip to content

Commit 9abba93

Browse files
temporary(datasecurity): fill all fields
1 parent b9d3ec2 commit 9abba93

7 files changed

Lines changed: 265 additions & 58 deletions

File tree

pkg/collector/sharedlibrary/rustchecks/checks/datasecurity/build.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,24 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
1515

1616
println!("cargo:rerun-if-changed={proto_file}");
1717

18-
prost_build::compile_protos(&[proto_file], &[proto_root])?;
18+
let mut config = prost_build::Config::new();
19+
20+
// TODO(DSEC): remove this serde derive once debugging ends — it only exists to
21+
// emit the SDS result payload as JSON (see `send_sds_result_json`), which we do
22+
// not need in production. `google.protobuf.Timestamp` (prost-types) has no serde
23+
// support, so its fields use a local `serialize_with` helper.
24+
config.type_attribute(".", "#[derive(serde::Serialize)]");
25+
for field in [
26+
".datadog.sds.SdsResultPayload.ScanMetadata.ScanTaskMetadata.started_at",
27+
".datadog.sds.SdsResultPayload.ScanMetadata.ScanTaskMetadata.ended_at",
28+
] {
29+
config.field_attribute(
30+
field,
31+
"#[serde(serialize_with = \"crate::proto::serialize_timestamp\")]",
32+
);
33+
}
34+
35+
config.compile_protos(&[proto_file], &[proto_root])?;
1936

2037
Ok(())
2138
}

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

Lines changed: 175 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
1+
use std::time::{SystemTime, UNIX_EPOCH};
2+
13
use anyhow::{Context, Result, anyhow};
2-
use core::*;
4+
use shlib_core::*;
5+
use serde_json::Value;
36

47
use crate::backend;
58
use crate::config::{CheckConfig, SubTask};
6-
use crate::payload::{Match, ScanEventPayload, ScanStatus};
9+
use crate::constants::SDS_RESULT_EVENT_TYPE;
10+
use crate::payload::{Match, ScanStatus};
11+
use crate::proto::{self, ScanMetadata, ScanResult, ScanTaskMetadata, SdsResultPayload, Status};
712
use crate::scanning::Scanner;
813

914
/// Check entrypoint.
@@ -45,56 +50,195 @@ fn run_sub_task(
4550
sub_task.sub_task_id, sub_task.entity.platform
4651
);
4752

53+
// Time the scan so the payload can carry started_at / ended_at / duration.
54+
let started_at = SystemTime::now();
55+
let scan = run_scan(scanner, sub_task);
56+
let ended_at = SystemTime::now();
57+
4858
// A sub task failure is reported inside the payload (status=ERROR) rather
4959
// than aborting the check, so every sub task produces exactly one event.
50-
let (status, failure_reason, matches) = match run_scan(scanner, sub_task) {
51-
Ok(matches) => {
60+
let (status, failure_reason, matches, scanned_row_count) = match scan {
61+
Ok(out) => {
5262
println!(
5363
"datasecurity: sub task succeeded ({} match(es))",
5464
matches.len()
5565
);
56-
(ScanStatus::Success, String::new(), matches)
66+
(ScanStatus::Success, String::new(), out.matches, out.scanned_row_count)
5767
}
5868
Err(err) => {
5969
let reason = format!("{err:#}");
6070
eprintln!(
6171
"datasecurity: sub task {} failed: {reason}",
6272
sub_task.sub_task_id
6373
);
64-
(ScanStatus::Error, reason, Vec::new())
74+
(ScanStatus::Error, reason, Vec::new(), 0)
6575
}
6676
};
6777

68-
let payload = ScanEventPayload {
69-
task_id: config.task_id.clone(),
70-
sub_task_id: sub_task.sub_task_id.clone(),
78+
// Build the SDS result protobuf: task metadata, timing, postgres location and
79+
// matches, mirroring the Data Observability crawler payload.
80+
let payload = build_sds_result(
81+
config,
82+
sub_task,
7183
status,
72-
failure_reason,
73-
matches,
74-
};
84+
&failure_reason,
85+
&matches,
86+
scanned_row_count,
87+
started_at,
88+
ended_at,
89+
);
90+
91+
// Emit the protobuf on the `sds-result` event platform track.
92+
if config.send_sds_result {
93+
check.event_platform_event_bytes(&proto::encode(&payload), SDS_RESULT_EVENT_TYPE)?;
94+
}
7595

76-
// TODO(DSEC-140): send sdsresult rather than an event
77-
let payload_json =
78-
serde_json::to_string(&payload).context("failed to serialize scan event payload")?;
79-
check.event(
80-
"datasecurity scan result",
81-
&payload_json,
82-
0,
83-
"normal",
84-
"",
85-
&[],
86-
"info",
87-
"",
88-
"datasecurity",
89-
"",
90-
)?;
96+
// TODO(DSEC): remove this JSON event once the protobuf is validated end to
97+
// end — we do not need to send SDS results as JSON. It is serialized from the
98+
// same protobuf so the two representations cannot drift.
99+
if config.send_sds_result_json {
100+
let payload_json =
101+
proto::to_json(&payload).context("failed to serialize sds result payload to json")?;
102+
check.event(
103+
"datasecurity scan result",
104+
&payload_json,
105+
0,
106+
"normal",
107+
"",
108+
&[],
109+
"info",
110+
"",
111+
"datasecurity",
112+
"",
113+
)?;
114+
}
91115

92116
Ok(())
93117
}
94118

95-
/// Fetches the sub task's data and scans it, returning the matches.
96-
/// TODO(dsec-161): add tests for the scan.
97-
fn run_scan(scanner: &Scanner, sub_task: &SubTask) -> Result<Vec<Match>> {
119+
/// Builds the `SdsResultPayload` protobuf for one sub task.
120+
///
121+
/// Mirrors the Data Observability crawler payload (`Resource`, `RuleIds`,
122+
/// `ScanningSource`, `ScanResults`), swapping the snowflake location for a
123+
/// postgres one and adding the scan-task metadata block.
124+
#[allow(clippy::too_many_arguments)]
125+
fn build_sds_result(
126+
config: &CheckConfig,
127+
sub_task: &SubTask,
128+
status: ScanStatus,
129+
failure_reason: &str,
130+
matches: &[Match],
131+
scanned_row_count: i64,
132+
started_at: SystemTime,
133+
ended_at: SystemTime,
134+
) -> SdsResultPayload {
135+
let entity = &sub_task.entity;
136+
let duration_ms = ended_at
137+
.duration_since(started_at)
138+
.map(|d| d.as_millis() as i64)
139+
.unwrap_or(0);
140+
141+
let location = proto::ScanLocation {
142+
scan_location: Some(proto::scan_location::ScanLocation::PostgresTable(
143+
proto::PostgresTable {
144+
database_cluster_name: entity.database_cluster_name.clone(),
145+
database_instance_name: entity.database_instance_name.clone(),
146+
database_host_name: sub_task.connection.host.clone(),
147+
database_name: entity.database.clone(),
148+
schema_name: entity.schema.clone(),
149+
table_name: entity.table.clone(),
150+
scanned_row_count,
151+
// TODO(DSEC): populate table_row_count (from DBM metadata) and
152+
// scanned_columns.
153+
..Default::default()
154+
},
155+
)),
156+
..Default::default()
157+
};
158+
159+
let scan_result = ScanResult {
160+
table_matches: proto::table_matches(matches),
161+
location: Some(location),
162+
duration: duration_ms,
163+
scan_metadata: Some(ScanMetadata {
164+
scan_task_metadata: Some(ScanTaskMetadata {
165+
task_id: config.task_id.clone(),
166+
sub_task_id: sub_task.sub_task_id.clone(),
167+
started_at: Some(proto::to_timestamp(started_at)),
168+
ended_at: Some(proto::to_timestamp(ended_at)),
169+
status: match status {
170+
ScanStatus::Success => Status::Success,
171+
ScanStatus::Error => Status::Error,
172+
} as i32,
173+
failure_reason: (!failure_reason.is_empty()).then(|| failure_reason.to_string()),
174+
}),
175+
}),
176+
..Default::default()
177+
};
178+
179+
SdsResultPayload {
180+
timestamp: now_unix_millis(),
181+
resource: Some(proto::Resource {
182+
r#type: "postgres_table".to_string(),
183+
name: resource_name(sub_task),
184+
}),
185+
rule_ids: config.scanning_rules.iter().map(|rule| rule.id.clone()).collect(),
186+
// The scanning source is the Agent. TODO(DSEC): populate hostname and
187+
// agent version once the check receives them (not provided via config yet).
188+
scanning_source: Some(proto::ScanningSource {
189+
source: Some(proto::scanning_source::Source::Agent(
190+
proto::scanning_source::Agent::default(),
191+
)),
192+
}),
193+
scan_results: vec![scan_result],
194+
..Default::default()
195+
}
196+
}
197+
198+
/// Result of a successful sub task scan.
199+
struct ScanOutput {
200+
matches: Vec<Match>,
201+
scanned_row_count: i64,
202+
}
203+
204+
/// Fetches the sub task's data and scans it, returning the matches and the
205+
/// number of rows scanned.
206+
fn run_scan(scanner: &Scanner, sub_task: &SubTask) -> Result<ScanOutput> {
98207
let data = backend::fetch_data(sub_task).context("fetching sub task data")?;
99-
scanner.scan(data).context("scanning sub task data")
208+
let matches = scanner.scan(data.clone()).context("scanning sub task data")?;
209+
Ok(ScanOutput {
210+
scanned_row_count: scanned_rows(&data),
211+
matches,
212+
})
213+
}
214+
215+
/// Number of rows scanned: the longest column array in the `{ column: [values] }`
216+
/// map returned by the backend.
217+
fn scanned_rows(data: &Value) -> i64 {
218+
data.as_object()
219+
.and_then(|columns| {
220+
columns
221+
.values()
222+
.filter_map(|value| value.as_array().map(Vec::len))
223+
.max()
224+
})
225+
.unwrap_or(0) as i64
226+
}
227+
228+
/// Current unix time in milliseconds, for the payload timestamp.
229+
fn now_unix_millis() -> i64 {
230+
SystemTime::now()
231+
.duration_since(UNIX_EPOCH)
232+
.map(|d| d.as_millis() as i64)
233+
.unwrap_or(0)
234+
}
235+
236+
/// Resource name (`<instance_name>.<database>.<schema>.<table>`), following the
237+
/// DO crawler convention.
238+
fn resource_name(sub_task: &SubTask) -> String {
239+
let entity = &sub_task.entity;
240+
format!(
241+
"{}.{}.{}.{}",
242+
entity.database_instance_name, entity.database, entity.schema, entity.table
243+
)
100244
}

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

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use anyhow::{Context, Result};
2-
use core::AgentCheck;
2+
use shlib_core::AgentCheck;
33
use serde::Deserialize;
44

55
use crate::scanning::ScanningRule;
@@ -19,6 +19,11 @@ impl CheckConfig {
1919
.instance
2020
.get("scan_data")
2121
.context("failed to read scan_data from instance config")?,
22+
// Emit the SDS result protobuf on the `sds-result` track (default on).
23+
send_sds_result: check.instance.get("send_sds_result").unwrap_or(true),
24+
// TODO(DSEC): debug only, off by default. Remove once the protobuf is
25+
// validated end to end.
26+
send_sds_result_json: check.instance.get("send_sds_result_json").unwrap_or(false),
2227
})
2328
}
2429
}
@@ -29,6 +34,16 @@ pub struct CheckConfig {
2934
pub task_id: String,
3035
pub scanning_rules: Vec<ScanningRule>,
3136
pub scan_data: Vec<SubTask>,
37+
/// Emit the SDS result protobuf on the `sds-result` event platform track.
38+
#[serde(default)]
39+
pub send_sds_result: bool,
40+
/// Emit the SDS result payload as JSON (serialized from the same protobuf) as
41+
/// a regular event.
42+
///
43+
/// TODO(DSEC): debug only, remove once the protobuf is validated end to end —
44+
/// we do not need to send SDS results as JSON.
45+
#[serde(default)]
46+
pub send_sds_result_json: bool,
3247
}
3348

3449
/// A single scan sub task: a query to run against one data source.
@@ -47,13 +62,28 @@ pub struct SubTask {
4762
pub timeout_seconds: u64,
4863
}
4964

50-
/// TODO(dsec-140): add the other entity values (scan location) when needed,
51-
/// e.g. database_cluster_name, database_instance_name, database, schema, table.
65+
/// The data asset a sub task targets, mirroring the Data Observability `entity`
66+
/// object (`comp/dataobs/queryactions`). Used to select the backend engine
67+
/// (`platform`) and to describe the scan location; not used to connect.
5268
#[derive(Debug, Default, Deserialize)]
5369
pub struct Entity {
5470
/// Data-source platform, used to select the backend engine (e.g. `postgres`).
5571
#[serde(default)]
5672
pub platform: String,
73+
/// Cloud cluster identifier of the data source (e.g. an RDS/Aurora cluster).
74+
/// Reported in the scan location.
75+
#[serde(default)]
76+
pub database_cluster_name: String,
77+
/// Cloud instance identifier of the data source; the DO entity `account`.
78+
/// Reported in the scan location.
79+
#[serde(default, alias = "account")]
80+
pub database_instance_name: String,
81+
#[serde(default)]
82+
pub database: String,
83+
#[serde(default)]
84+
pub schema: String,
85+
#[serde(default)]
86+
pub table: String,
5787
}
5888

5989
fn default_timeout_seconds() -> u64 {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use core::generate_ffi;
1+
use shlib_core::generate_ffi;
22

33
mod backend;
44
mod check;

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

Lines changed: 4 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,12 @@
1-
use serde::Serialize;
2-
3-
/// JSON event payload emitted by the check, one per sub task.
4-
///
5-
/// TODO(dsec-140): replace with the SDS result payload (protoc generated code).
6-
#[derive(Debug, Serialize)]
7-
#[serde(rename_all = "snake_case")]
8-
pub struct ScanEventPayload {
9-
pub task_id: String,
10-
pub sub_task_id: String,
11-
pub status: ScanStatus,
12-
#[serde(skip_serializing_if = "String::is_empty")]
13-
pub failure_reason: String,
14-
pub matches: Vec<Match>,
15-
}
16-
17-
/// TODO(dsec-140): replace with the SDS result payload task status (protoc generated code).
18-
#[derive(Debug, Clone, Copy, Serialize)]
19-
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
1+
/// Outcome of a sub task scan, mirroring the proto `ScanStatus`.
2+
#[derive(Debug, Clone, Copy)]
203
pub enum ScanStatus {
214
Success,
225
Error,
236
}
247

25-
#[derive(Debug, Serialize, PartialEq)]
26-
#[serde(rename_all = "snake_case")]
8+
/// Aggregated scanner match for one column, converted into a proto `TableMatch`.
9+
#[derive(Debug, PartialEq)]
2710
pub struct Match {
2811
pub rule_id: String,
2912
pub column_name: String,

0 commit comments

Comments
 (0)