Skip to content

Commit d856f66

Browse files
datasecurity: simplify code and tests
1 parent 83267b2 commit d856f66

4 files changed

Lines changed: 46 additions & 70 deletions

File tree

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

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,6 @@ impl CheckConfig {
2626

2727

2828
/// Instance configuration for the datasecurity check.
29-
///
30-
/// Kept in its own module so the deserialization surface of the check instance
31-
/// config stays readable and easy to grow as the check gains real RC tasks.
3229
#[derive(Debug, Default, Deserialize)]
3330
pub struct CheckConfig {
3431
#[serde(default)]

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

Lines changed: 13 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
1-
//! Self-contained SDS scanning: rule config parsing (`rule`) plus building and
2-
//! running the dd-sds scanner over column-oriented query results. Everything
3-
//! scanning-related lives in this module so the rest of the check stays lean.
1+
//! SDS scanning: parse rules and run the dd-sds scanner over query results.
42
53
use std::collections::{HashMap, HashSet};
64
use std::sync::Arc;
75

86
use anyhow::{Context, Result};
97
use dd_sds::{RootRuleConfig, RuleConfig, RuleMatch, Scanner as SdsScanner, ScannerBuilder};
10-
use serde_json::{Map, Value};
8+
use serde_json::Value;
119

1210
use crate::payload::Match;
1311

@@ -17,64 +15,44 @@ pub use rule::ScanningRule;
1715
#[cfg(test)]
1816
mod tests;
1917

20-
/// A built dd-sds scanner plus the rule ids in builder order. dd-sds reports
21-
/// matches by rule index, so we keep the ids to map matches back to our rules.
18+
/// A dd-sds scanner plus the rule ids, used to map matches back to rules.
2219
pub struct Scanner {
2320
scanner: SdsScanner,
2421
rule_ids: Vec<String>,
2522
}
2623

2724
impl Scanner {
28-
/// Builds a scanner from the check's scanning rules. The full dd-sds rule
29-
/// surface (pattern, proximity keywords, suppressions, precedence, secondary
30-
/// validators such as Luhn/JWT, ...) is supported because each rule carries
31-
/// the flattened `RootRuleConfig` verbatim.
25+
/// Builds a scanner from the check's scanning rules.
3226
pub fn new(rules: &[ScanningRule]) -> Result<Self> {
3327
let mut rule_ids = Vec::with_capacity(rules.len());
34-
let compiled: Vec<RootRuleConfig<Arc<dyn RuleConfig>>> = rules
28+
let scanner_rules: Vec<RootRuleConfig<Arc<dyn RuleConfig>>> = rules
3529
.iter()
3630
.map(|rule| {
3731
rule_ids.push(rule.id.clone());
3832
rule.config.clone().into_dyn()
3933
})
4034
.collect();
4135

42-
let scanner = ScannerBuilder::new(&compiled)
36+
let scanner = ScannerBuilder::new(&scanner_rules)
4337
.build()
4438
.context("building sds scanner")?;
4539
Ok(Self { scanner, rule_ids })
4640
}
4741

48-
/// Scans a column-oriented result (`{ column: [values...] }`) and returns
49-
/// one `Match` per (column, rule) pair with the count of matched rows.
42+
/// Scans `{ column: [values] }` and returns one `Match` per (column, rule).
5043
pub fn scan(&self, data: &Value) -> Result<Vec<Match>> {
51-
let mut event = Map::new();
52-
if let Some(columns) = data.as_object() {
53-
for (name, values) in columns {
54-
let str_values: Vec<Value> = values
55-
.as_array()
56-
.map(|vs| {
57-
vs.iter()
58-
.filter_map(|v| v.as_str().map(|s| Value::String(s.to_string())))
59-
.collect()
60-
})
61-
.unwrap_or_default();
62-
event.insert(name.clone(), Value::Array(str_values));
63-
}
64-
}
65-
66-
let mut event_value = Value::Object(event);
44+
let mut event = data.clone();
6745
let hits = self
6846
.scanner
69-
.scan(&mut event_value)
47+
.scan(&mut event)
7048
.context("scanning query result")?;
7149

7250
Ok(aggregate_matches(&self.rule_ids, &hits))
7351
}
7452
}
7553

76-
/// Groups raw dd-sds hits into `(column, rule)` pairs, counting distinct matched
77-
/// rows (paths) per pair. Output is sorted so emitted events are deterministic.
54+
/// Groups hits into `(column, rule)` pairs and counts matched rows. Sorted for
55+
/// deterministic output.
7856
fn aggregate_matches(rule_ids: &[String], hits: &[RuleMatch]) -> Vec<Match> {
7957
let mut rows: HashMap<(String, String), HashSet<String>> = HashMap::new();
8058
for hit in hits {
@@ -101,9 +79,9 @@ fn aggregate_matches(rule_ids: &[String], hits: &[RuleMatch]) -> Vec<Match> {
10179
matches
10280
}
10381

104-
/// `foo[0]` / `foo.bar` -> `foo`.
82+
/// `foo[0]` -> `foo`.
10583
fn column_name_from_path(path: &str) -> String {
106-
match path.find(['[', '.']) {
84+
match path.find('[') {
10785
Some(i) => path[..i].to_string(),
10886
None => path.to_string(),
10987
}

pkg/collector/sharedlibrary/rustchecks/checks/datasecurity/src/scanning/rule.rs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,6 @@
11
use dd_sds::{RegexRuleConfig, RootRuleConfig};
22
use serde::Deserialize;
33

4-
/// A scanning rule, shared by every sub task. `id` (used to map matches back to
5-
/// the rule) and `name` are ours; everything else is the flattened dd-sds
6-
/// `RootRuleConfig<RegexRuleConfig>`, so the full rule schema — pattern,
7-
/// proximity keywords, suppressions, precedence and secondary validators (Luhn,
8-
/// JWT, ...) — comes straight from dd-sds with no duplication.
94
#[derive(Debug, Deserialize)]
105
pub struct ScanningRule {
116
pub id: String,

pkg/collector/sharedlibrary/rustchecks/checks/datasecurity/src/scanning/tests.rs

Lines changed: 33 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,5 @@
1-
//! Scanning tests, split in two parts:
2-
//! * config parsing — a `ScanningRule` deserializes straight into the
3-
//! flattened dd-sds `RootRuleConfig<RegexRuleConfig>`, so the whole dd-sds
4-
//! rule surface (proximity keywords, suppressions, precedence, secondary
5-
//! validators) is accepted verbatim and a scanner builds from it;
6-
//! * scanning behaviour — running the built scanner over column-oriented data
7-
//! yields the expected per-(column, rule) matched-row counts.
8-
91
use dd_sds::{MatchAction, SecondaryValidator};
10-
use serde_json::json;
2+
use serde_json::{json, Value};
113

124
use super::{Scanner, ScanningRule};
135

@@ -19,6 +11,13 @@ fn scanner(yaml: &str) -> Scanner {
1911
Scanner::new(&parse(yaml)).expect("build scanner")
2012
}
2113

14+
/// Scans `data` and returns the matches as JSON, so tests can assert against a
15+
/// single `json!` literal.
16+
fn scan_json(scanner: &Scanner, data: &Value) -> Value {
17+
let matches = scanner.scan(data).expect("scan");
18+
serde_json::to_value(matches).expect("serialize matches")
19+
}
20+
2221
// --- config parsing -------------------------------------------------------
2322

2423
#[test]
@@ -163,12 +162,13 @@ fn suppression_drops_matching_rows() {
163162
]
164163
});
165164

166-
let matches = scanner.scan(&data).expect("scan");
167-
assert_eq!(matches.len(), 1, "one (column, rule) pair");
168-
assert_eq!(matches[0].rule_id, "email");
169-
assert_eq!(matches[0].column_name, "email");
170165
// `alice@example.com` is suppressed; the other two rows match.
171-
assert_eq!(matches[0].count_matched_rows, 2);
166+
assert_eq!(
167+
scan_json(&scanner, &data),
168+
json!([
169+
{ "rule_id": "email", "column_name": "email", "count_matched_rows": 2 }
170+
])
171+
);
172172
}
173173

174174
#[test]
@@ -191,11 +191,13 @@ fn included_keyword_required_for_match() {
191191
]
192192
});
193193

194-
let matches = scanner.scan(&data).expect("scan");
195-
assert_eq!(matches.len(), 1);
196-
assert_eq!(matches[0].rule_id, "token");
197194
// Only the row with the `token` keyword nearby matches.
198-
assert_eq!(matches[0].count_matched_rows, 1);
195+
assert_eq!(
196+
scan_json(&scanner, &data),
197+
json!([
198+
{ "rule_id": "token", "column_name": "note", "count_matched_rows": 1 }
199+
])
200+
);
199201
}
200202

201203
#[test]
@@ -218,10 +220,13 @@ fn excluded_keyword_suppresses_match() {
218220
]
219221
});
220222

221-
let matches = scanner.scan(&data).expect("scan");
222-
assert_eq!(matches.len(), 1);
223223
// The row preceded by the `test` keyword is excluded.
224-
assert_eq!(matches[0].count_matched_rows, 1);
224+
assert_eq!(
225+
scan_json(&scanner, &data),
226+
json!([
227+
{ "rule_id": "code", "column_name": "code", "count_matched_rows": 1 }
228+
])
229+
);
225230
}
226231

227232
#[test]
@@ -243,11 +248,13 @@ fn luhn_checksum_filters_invalid_numbers() {
243248
]
244249
});
245250

246-
let matches = scanner.scan(&data).expect("scan");
247-
assert_eq!(matches.len(), 1);
248-
assert_eq!(matches[0].rule_id, "credit-card");
249251
// Only the Luhn-valid number is kept.
250-
assert_eq!(matches[0].count_matched_rows, 1);
252+
assert_eq!(
253+
scan_json(&scanner, &data),
254+
json!([
255+
{ "rule_id": "credit-card", "column_name": "card", "count_matched_rows": 1 }
256+
])
257+
);
251258
}
252259

253260
#[test]
@@ -261,6 +268,5 @@ fn no_matches_produce_empty() {
261268
);
262269

263270
let data = json!({ "name": ["alice", "bob"] });
264-
let matches = scanner.scan(&data).expect("scan");
265-
assert!(matches.is_empty());
271+
assert_eq!(scan_json(&scanner, &data), json!([]));
266272
}

0 commit comments

Comments
 (0)