Skip to content

Commit 2235125

Browse files
temporary(datasecurity): fill all fields
1 parent bdf100e commit 2235125

8 files changed

Lines changed: 279 additions & 63 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/backend/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,9 @@ fn engine_for(platform: &str) -> Result<&'static dyn ScanEngine> {
3333
.with_context(|| format!("unsupported platform {platform:?}"))
3434
}
3535

36-
/// Runs the sub task on the engine selected by its platform.
36+
/// Runs the sub task on the engine selected by its entity platform.
3737
pub fn fetch_data(sub_task: &SubTask) -> Result<Value> {
38-
engine_for(&sub_task.platform)?.fetch_data(sub_task)
38+
engine_for(&sub_task.entity.platform)?.fetch_data(sub_task)
3939
}
4040

4141
#[cfg(all(test, feature = "engine-postgres"))]
Lines changed: 177 additions & 33 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.
@@ -42,56 +47,195 @@ fn run_sub_task(
4247
) -> Result<()> {
4348
println!(
4449
"datasecurity: running sub task (sub_task_id={}, platform={})",
45-
sub_task.sub_task_id, sub_task.platform
50+
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) => {
52-
println!("datasecurity: sub task succeeded ({} match(es))", matches.len());
53-
(ScanStatus::Success, String::new(), matches)
60+
let (status, failure_reason, matches, scanned_row_count) = match scan {
61+
Ok(out) => {
62+
println!("datasecurity: sub task succeeded ({} match(es))", out.matches.len());
63+
(ScanStatus::Success, String::new(), out.matches, out.scanned_row_count)
5464
}
5565
Err(err) => {
5666
let reason = format!("{err:#}");
5767
eprintln!(
5868
"datasecurity: sub task {} failed: {reason}",
5969
sub_task.sub_task_id
6070
);
61-
(ScanStatus::Error, reason, Vec::new())
71+
(ScanStatus::Error, reason, Vec::new(), 0)
6272
}
6373
};
6474

65-
let payload = ScanEventPayload {
66-
task_id: config.task_id.clone(),
67-
sub_task_id: sub_task.sub_task_id.clone(),
75+
// Build the SDS result protobuf: task metadata, timing, postgres location and
76+
// matches, mirroring the Data Observability crawler payload.
77+
let payload = build_sds_result(
78+
config,
79+
sub_task,
6880
status,
69-
failure_reason,
70-
matches,
71-
};
81+
&failure_reason,
82+
&matches,
83+
scanned_row_count,
84+
started_at,
85+
ended_at,
86+
);
87+
88+
// Emit the protobuf on the `sds-result` event platform track.
89+
if config.send_sds_result {
90+
check.event_platform_event_bytes(&proto::encode(&payload), SDS_RESULT_EVENT_TYPE)?;
91+
}
7292

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

89113
Ok(())
90114
}
91115

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

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

Lines changed: 44 additions & 4 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
}
@@ -30,17 +35,28 @@ pub struct CheckConfig {
3035
pub task_id: String,
3136
pub scanning_rules: Vec<ScanningRule>,
3237
pub scan_data: Vec<SubTask>,
38+
/// Emit the SDS result protobuf on the `sds-result` event platform track.
39+
#[serde(default)]
40+
pub send_sds_result: bool,
41+
/// Emit the SDS result payload as JSON (serialized from the same protobuf) as
42+
/// a regular event.
43+
///
44+
/// TODO(DSEC): debug only, remove once the protobuf is validated end to end —
45+
/// we do not need to send SDS results as JSON.
46+
#[serde(default)]
47+
pub send_sds_result_json: bool,
3348
}
3449

3550
/// A single scan sub task: a query to run against one data source.
3651
#[derive(Debug, Default, Deserialize)]
3752
pub struct SubTask {
3853
pub sub_task_id: String,
39-
/// Data-source platform, used to select the backend engine (e.g. `postgres`).
40-
#[serde(default)]
41-
pub platform: String,
4254
#[serde(default)]
4355
pub connection: Connection,
56+
/// Data asset the sub task targets. Mirrors the Data Observability
57+
/// `entity` object and is reported in the scan location.
58+
#[serde(default)]
59+
pub entity: Entity,
4460
/// SQL query whose result columns are scanned.
4561
#[serde(default)]
4662
pub query: String,
@@ -49,6 +65,30 @@ pub struct SubTask {
4965
pub timeout_seconds: u64,
5066
}
5167

68+
/// The data asset a sub task targets, mirroring the Data Observability `entity`
69+
/// object (`comp/dataobs/queryactions`). Used to select the backend engine
70+
/// (`platform`) and to describe the scan location; not used to connect.
71+
#[derive(Debug, Default, Deserialize)]
72+
pub struct Entity {
73+
/// Data-source platform, used to select the backend engine (e.g. `postgres`).
74+
#[serde(default)]
75+
pub platform: String,
76+
/// Cloud cluster identifier of the data source (e.g. an RDS/Aurora cluster).
77+
/// Reported in the scan location.
78+
#[serde(default)]
79+
pub database_cluster_name: String,
80+
/// Cloud instance identifier of the data source; the DO entity `account`.
81+
/// Reported in the scan location.
82+
#[serde(default, alias = "account")]
83+
pub database_instance_name: String,
84+
#[serde(default)]
85+
pub database: String,
86+
#[serde(default)]
87+
pub schema: String,
88+
#[serde(default)]
89+
pub table: String,
90+
}
91+
5292
fn default_timeout_seconds() -> u64 {
5393
30
5494
}

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;

0 commit comments

Comments
 (0)