@@ -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