Skip to content

Commit 690b4a8

Browse files
committed
Supporting Regex in Policy
1 parent 02c8a22 commit 690b4a8

File tree

3 files changed

+218
-11
lines changed

3 files changed

+218
-11
lines changed

src/automation/mod.rs

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,10 +112,14 @@ pub struct ScanPolicy {
112112
#[derive(Debug, Clone, Serialize, Deserialize)]
113113
pub struct PolicyConditions {
114114
pub title_contains: Option<Vec<String>>, // Keywords like "unauth", "xss"
115+
pub title_regex: Option<Vec<String>>, // Regex patterns for title matching
116+
pub description_contains: Option<Vec<String>>, // Keywords in vulnerability description
117+
pub description_regex: Option<Vec<String>>, // Regex patterns for description matching
115118
pub severity: Option<Vec<String>>, // "critical", "high", "medium", "low"
116119
pub ecosystems: Option<Vec<Ecosystem>>,
117120
pub cve_pattern: Option<String>, // Regex pattern for CVE IDs
118121
pub packages: Option<Vec<String>>, // Specific package names
122+
pub package_regex: Option<Vec<String>>, // Regex patterns for package names
119123
}
120124

121125
/// Policy actions when conditions are met
@@ -125,6 +129,7 @@ pub struct PolicyActions {
125129
pub priority: PolicyPriority,
126130
pub custom_message: Option<String>,
127131
pub ignore: bool, // Ignore vulnerabilities matching this policy
132+
pub filter_only: bool, // If true, ONLY show vulnerabilities that match this policy (whitelist mode)
128133
}
129134

130135
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -236,6 +241,50 @@ impl ScanPolicy {
236241
}
237242
}
238243

244+
// Check title regex patterns
245+
if let Some(patterns) = &conditions.title_regex {
246+
let title = &vulnerability.summary;
247+
let matches_pattern = patterns.iter().any(|pattern| {
248+
match Regex::new(pattern) {
249+
Ok(regex) => regex.is_match(title),
250+
Err(_) => {
251+
// Log warning but don't fail the match
252+
false
253+
}
254+
}
255+
});
256+
if !matches_pattern {
257+
return false;
258+
}
259+
}
260+
261+
// Check description contains keywords (if vulnerability has description field)
262+
if let Some(keywords) = &conditions.description_contains {
263+
// Note: Vulnerability struct might not have a separate description field
264+
// For now, we'll check against the summary field as well
265+
let text_to_search = vulnerability.summary.to_lowercase();
266+
let has_keyword = keywords.iter().any(|keyword| {
267+
text_to_search.contains(&keyword.to_lowercase())
268+
});
269+
if !has_keyword {
270+
return false;
271+
}
272+
}
273+
274+
// Check description regex patterns
275+
if let Some(patterns) = &conditions.description_regex {
276+
let text_to_search = &vulnerability.summary;
277+
let matches_pattern = patterns.iter().any(|pattern| {
278+
match Regex::new(pattern) {
279+
Ok(regex) => regex.is_match(text_to_search),
280+
Err(_) => false,
281+
}
282+
});
283+
if !matches_pattern {
284+
return false;
285+
}
286+
}
287+
239288
// Check severity
240289
if let Some(severities) = &conditions.severity {
241290
if let Some(vuln_severity) = &vulnerability.severity {
@@ -257,12 +306,18 @@ impl ScanPolicy {
257306
}
258307
}
259308

260-
// Check packages
309+
// Check packages (basic string matching)
261310
if let Some(_packages) = &conditions.packages {
262311
// This would need to be checked against the package name from scan context
263312
// For now, we'll skip this check as we don't have package context in Vulnerability
264313
}
265314

315+
// Check package regex patterns
316+
if let Some(_patterns) = &conditions.package_regex {
317+
// This would need to be checked against the package name from scan context
318+
// For now, we'll skip this check as we don't have package context in Vulnerability
319+
}
320+
266321
true
267322
}
268323
}

src/automation/policy.rs

Lines changed: 149 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,49 @@ impl PolicyEngine {
4141
.map(|p| (format!("{}@{}", p.name, p.version), p))
4242
.collect();
4343

44-
// Apply each policy to each vulnerability
44+
// Check if any policies have filter_only=true (whitelist mode)
45+
let has_filter_only_policies = self.policies.iter().any(|p| p.enabled && p.actions.filter_only);
46+
let mut filter_only_matched_vulns = std::collections::HashSet::new();
47+
48+
// First pass: collect vulnerabilities that match filter_only policies
49+
if has_filter_only_policies {
50+
for vulnerability in &scan_result.vulnerabilities {
51+
for policy in &self.policies {
52+
if policy.enabled && policy.actions.filter_only {
53+
let package_name = self.find_package_for_vulnerability(vulnerability, &package_map);
54+
55+
if self.vulnerability_matches_policy(vulnerability, policy, package_name.as_deref()) {
56+
filter_only_matched_vulns.insert(vulnerability.id.clone());
57+
58+
let policy_match = PolicyMatch {
59+
policy_name: policy.name.clone(),
60+
vulnerability_id: vulnerability.id.clone(),
61+
package_name: package_name.clone().unwrap_or_else(|| "unknown".to_string()),
62+
actions: policy.actions.clone(),
63+
};
64+
policy_matches.push(policy_match);
65+
66+
if policy.actions.notify {
67+
prioritized_vulnerabilities.push(vulnerability.id.clone());
68+
info!("Filter-only policy '{}' matched and prioritized vulnerability: {} ({})",
69+
policy.name, vulnerability.id, policy.actions.priority.as_str());
70+
}
71+
break; // One filter_only match is enough to include the vulnerability
72+
}
73+
}
74+
}
75+
}
76+
}
77+
78+
// Second pass: apply regular policies to remaining vulnerabilities
4579
for vulnerability in &scan_result.vulnerabilities {
80+
// If we have filter_only policies and this vulnerability didn't match any, skip it
81+
if has_filter_only_policies && !filter_only_matched_vulns.contains(&vulnerability.id) {
82+
continue;
83+
}
84+
4685
for policy in &self.policies {
47-
if policy.enabled {
86+
if policy.enabled && !policy.actions.filter_only {
4887
// Find the package associated with this vulnerability
4988
let package_name = self.find_package_for_vulnerability(vulnerability, &package_map);
5089

@@ -74,18 +113,31 @@ impl PolicyEngine {
74113
}
75114
}
76115

77-
// Remove ignored vulnerabilities from the result
116+
// Filter vulnerabilities based on policy results
78117
let filtered_vulnerabilities: Vec<Vulnerability> = scan_result
79118
.vulnerabilities
80119
.iter()
81-
.filter(|v| !ignored_vulnerabilities.contains(&v.id))
120+
.filter(|v| {
121+
// If we have filter_only policies, only include vulnerabilities that matched them
122+
if has_filter_only_policies {
123+
filter_only_matched_vulns.contains(&v.id) && !ignored_vulnerabilities.contains(&v.id)
124+
} else {
125+
// Normal mode: exclude only ignored vulnerabilities
126+
!ignored_vulnerabilities.contains(&v.id)
127+
}
128+
})
82129
.cloned()
83130
.collect();
84131

85132
let mut filtered_scan_result = scan_result.clone();
86133
filtered_scan_result.vulnerabilities = filtered_vulnerabilities;
87134
filtered_scan_result.policies_applied = self.policies.iter().map(|p| p.name.clone()).collect();
88135

136+
info!("Policy filtering results: {} vulnerabilities -> {} vulnerabilities (filter_only_mode: {})",
137+
scan_result.vulnerabilities.len(),
138+
filtered_scan_result.vulnerabilities.len(),
139+
has_filter_only_policies);
140+
89141
FilteredScanResult {
90142
scan_result: filtered_scan_result,
91143
policy_matches,
@@ -112,6 +164,57 @@ impl PolicyEngine {
112164
}
113165
}
114166

167+
// Check title regex patterns
168+
if let Some(patterns) = &conditions.title_regex {
169+
let title = &vulnerability.summary;
170+
let matches_pattern = patterns.iter().any(|pattern| {
171+
match Regex::new(pattern) {
172+
Ok(regex) => regex.is_match(title),
173+
Err(e) => {
174+
warn!("Invalid regex pattern in policy '{}': {} - Error: {}", policy.name, pattern, e);
175+
false
176+
}
177+
}
178+
});
179+
180+
if !matches_pattern {
181+
return false;
182+
}
183+
}
184+
185+
// Check description contains keywords
186+
if let Some(keywords) = &conditions.description_contains {
187+
// For now, we'll search in the summary field since Vulnerability might not have separate description
188+
let text_to_search = vulnerability.summary.to_lowercase();
189+
190+
let has_keyword = keywords.iter().any(|keyword| {
191+
let keyword_lower = keyword.to_lowercase();
192+
text_to_search.contains(&keyword_lower)
193+
});
194+
195+
if !has_keyword {
196+
return false;
197+
}
198+
}
199+
200+
// Check description regex patterns
201+
if let Some(patterns) = &conditions.description_regex {
202+
let text_to_search = &vulnerability.summary;
203+
let matches_pattern = patterns.iter().any(|pattern| {
204+
match Regex::new(pattern) {
205+
Ok(regex) => regex.is_match(text_to_search),
206+
Err(e) => {
207+
warn!("Invalid regex pattern in policy '{}': {} - Error: {}", policy.name, pattern, e);
208+
false
209+
}
210+
}
211+
});
212+
213+
if !matches_pattern {
214+
return false;
215+
}
216+
}
217+
115218
// Check severity levels
116219
if let Some(severities) = &conditions.severity {
117220
if let Some(vuln_severity) = &vulnerability.severity {
@@ -168,6 +271,28 @@ impl PolicyEngine {
168271
}
169272
}
170273

274+
// Check package regex patterns
275+
if let Some(patterns) = &conditions.package_regex {
276+
if let Some(pkg_name) = package_name {
277+
let matches_pattern = patterns.iter().any(|pattern| {
278+
match Regex::new(pattern) {
279+
Ok(regex) => regex.is_match(pkg_name),
280+
Err(e) => {
281+
warn!("Invalid package regex pattern in policy '{}': {} - Error: {}", policy.name, pattern, e);
282+
false
283+
}
284+
}
285+
});
286+
287+
if !matches_pattern {
288+
return false;
289+
}
290+
} else {
291+
// No package name available, can't match package regex condition
292+
return false;
293+
}
294+
}
295+
171296
// Check ecosystems
172297
if let Some(_ecosystems) = &conditions.ecosystems {
173298
// This would need ecosystem context from the scan
@@ -208,16 +333,21 @@ impl PolicyEngine {
208333
"privilege".to_string(),
209334
"escalation".to_string(),
210335
]),
336+
title_regex: None,
337+
description_contains: None,
338+
description_regex: None,
211339
severity: Some(vec!["high".to_string(), "critical".to_string()]),
212340
ecosystems: None,
213341
cve_pattern: None,
214342
packages: None,
343+
package_regex: None,
215344
},
216345
actions: PolicyActions {
217346
notify: true,
218347
priority: crate::automation::PolicyPriority::Critical,
219348
custom_message: Some("🚨 Critical authentication vulnerability detected!".to_string()),
220349
ignore: false,
350+
filter_only: false,
221351
},
222352
},
223353
ScanPolicy {
@@ -229,16 +359,21 @@ impl PolicyEngine {
229359
"cross-site scripting".to_string(),
230360
"script injection".to_string(),
231361
]),
362+
title_regex: None,
363+
description_contains: None,
364+
description_regex: None,
232365
severity: Some(vec!["medium".to_string(), "high".to_string(), "critical".to_string()]),
233366
ecosystems: None,
234367
cve_pattern: None,
235368
packages: None,
369+
package_regex: None,
236370
},
237371
actions: PolicyActions {
238372
notify: true,
239373
priority: crate::automation::PolicyPriority::High,
240374
custom_message: Some("⚠️ XSS vulnerability requires attention".to_string()),
241375
ignore: false,
376+
filter_only: false,
242377
},
243378
},
244379
ScanPolicy {
@@ -250,23 +385,31 @@ impl PolicyEngine {
250385
"sqli".to_string(),
251386
"sql".to_string(),
252387
]),
388+
title_regex: None,
389+
description_contains: None,
390+
description_regex: None,
253391
severity: Some(vec!["medium".to_string(), "high".to_string(), "critical".to_string()]),
254392
ecosystems: None,
255393
cve_pattern: None,
256394
packages: None,
395+
package_regex: None,
257396
},
258397
actions: PolicyActions {
259398
notify: true,
260399
priority: crate::automation::PolicyPriority::High,
261400
custom_message: Some("💉 SQL injection vulnerability detected".to_string()),
262401
ignore: false,
402+
filter_only: false,
263403
},
264404
},
265405
ScanPolicy {
266406
name: "Low Priority Development Dependencies".to_string(),
267407
enabled: true,
268408
conditions: PolicyConditions {
269409
title_contains: None,
410+
title_regex: None,
411+
description_contains: None,
412+
description_regex: None,
270413
severity: Some(vec!["low".to_string()]),
271414
ecosystems: None,
272415
cve_pattern: None,
@@ -276,12 +419,14 @@ impl PolicyEngine {
276419
"*dev".to_string(),
277420
"*test".to_string(),
278421
]),
422+
package_regex: None,
279423
},
280424
actions: PolicyActions {
281425
notify: false,
282426
priority: crate::automation::PolicyPriority::Low,
283427
custom_message: None,
284428
ignore: true, // Ignore low-severity issues in dev dependencies
429+
filter_only: false,
285430
},
286431
},
287432
]

0 commit comments

Comments
 (0)