Skip to content

Commit 4dcc229

Browse files
committed
feat(suppression): v2.5.5 Phase 2 — analyzer wire-up
ROADMAP v2.5.5 Phase 2 — turns the foundation modules (test_context, comment_marker, ffi_kind, jit_context) into actual FP reduction. What this PR does: - Adds `apply_v255_context_suppression(&mut report)` in src/assail/mod.rs, called automatically from `apply_suppression` AFTER the kanren-based rule pass. - Per-finding logic: 1. Marker-based suppression — if a `panic-attack: accepted` marker is on the same or preceding line, flip suppressed = true. 2. PanicPath in TestOnly/Doc — auto-suppress via test_context classification. 3. UnsafeFFI in BuildSystem/TestMock — auto-suppress via FfiKind::is_audit_accepted_by_default(). - Sets WeakPoint.test_context for EVERY finding (even non-suppressible ones) so downstream audit consumers can render classification. - Per-file content cache to avoid repeated disk reads. What this PR does NOT do (future slices): - JitContext wire-up — the analyzer's inline Cranelift check at src/assail/analyzer.rs:1117..1129 still works correctly; consolidating with classify_rust() is a code-cleanup follow-up, not an FP reducer. - Marker support in /* ... */ block comments — line comments cover the common case. - Dedicated WeakPointCategory for "comment-only review" — the four v2.5.5 ROADMAP items (3 of 4 by my count) are deferred per the semantic-mismatch analysis in PR #107. main.rs side: - Added `mod comment_marker; mod ffi_kind; mod jit_context; mod test_context;` so the binary crate can see them too (consistent with how the bin already declares mod assail / mod types alongside the lib's pub mod). Verification: - 359 lib tests pass (353 baseline + 6 new v255_context_suppression tests covering test-file PanicPath suppression, prod-file pass-through, build.zig UnsafeFFI suppression, bindings/ pass-through, filesystem marker scanning, already-suppressed not-recounted).
1 parent d25210d commit 4dcc229

2 files changed

Lines changed: 233 additions & 0 deletions

File tree

src/assail/mod.rs

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,9 +390,111 @@ pub fn apply_suppression(report: &mut AssailReport) {
390390
}
391391
}
392392

393+
// v2.5.5 context-aware suppression pass — uses the test_context /
394+
// comment_marker / ffi_kind / jit_context foundation modules to
395+
// classify each finding's location and flip suppressed = true for
396+
// findings that fall in a known-acceptable context. Runs AFTER the
397+
// kanren-based rules above so kanren rule explanations remain the
398+
// authoritative source for findings suppressed by structural rules.
399+
count += apply_v255_context_suppression(report);
400+
393401
report.suppressed_count = count;
394402
}
395403

404+
/// v2.5.5 context-aware FP suppression pass — applies four checks per
405+
/// finding:
406+
/// 1. Inline `panic-attack: accepted` marker on the same or
407+
/// preceding line ([[comment_marker]]).
408+
/// 2. PanicPath finding in a TestOnly or Doc test_context
409+
/// ([[test_context]]).
410+
/// 3. UnsafeFFI finding where the file's [[ffi_kind]] is
411+
/// `BuildSystem` or `TestMock` (audit-accepted by default).
412+
/// 4. (jit_context wire-up handled inline in analyzer.rs:1117..1129
413+
/// already; not duplicated here.)
414+
///
415+
/// Sets `wp.test_context` for every finding with a known file path so
416+
/// downstream audit consumers can render it. Returns the number of
417+
/// findings newly flipped from `suppressed=false` to `suppressed=true`
418+
/// (already-suppressed findings are NOT re-counted).
419+
pub fn apply_v255_context_suppression(report: &mut AssailReport) -> usize {
420+
use crate::comment_marker;
421+
use crate::ffi_kind::FfiKind;
422+
use crate::test_context;
423+
use crate::types::{TestContext, WeakPointCategory};
424+
425+
let mut newly_suppressed = 0usize;
426+
let mut content_cache: std::collections::HashMap<String, Option<String>> =
427+
std::collections::HashMap::new();
428+
429+
for wp in &mut report.weak_points {
430+
// Determine the file path; prefer the structured `file` field,
431+
// fall back to parsing `location` ("path:line").
432+
let file_path: Option<String> = wp.file.clone().or_else(|| {
433+
wp.location.as_ref().and_then(|loc| {
434+
loc.rsplit_once(':').map(|(p, _)| p.to_string()).or_else(|| Some(loc.clone()))
435+
})
436+
});
437+
438+
let Some(file) = file_path else { continue };
439+
440+
// Load (and cache) the file's content for marker scanning. Files
441+
// not on disk (synthetic locations, removed-since-scan) cache
442+
// None so we don't repeatedly retry.
443+
let content_opt = content_cache
444+
.entry(file.clone())
445+
.or_insert_with(|| std::fs::read_to_string(&file).ok())
446+
.clone();
447+
448+
// Always set test_context — even when the finding isn't a
449+
// suppression candidate, downstream consumers benefit from the
450+
// classification metadata.
451+
if wp.test_context.is_none() {
452+
let ctx = match content_opt.as_deref() {
453+
Some(c) => test_context::classify(&file, c),
454+
None => test_context::classify_path(&file),
455+
};
456+
wp.test_context = Some(ctx);
457+
}
458+
459+
if wp.suppressed {
460+
continue;
461+
}
462+
463+
// (1) Marker-based suppression.
464+
if let (Some(content), Some(line)) = (content_opt.as_deref(), wp.line) {
465+
if comment_marker::is_suppressed_at(content, line).is_some() {
466+
wp.suppressed = true;
467+
newly_suppressed += 1;
468+
continue;
469+
}
470+
}
471+
472+
// (2) PanicPath in test/doc scope.
473+
if matches!(wp.category, WeakPointCategory::PanicPath)
474+
&& matches!(
475+
wp.test_context,
476+
Some(TestContext::TestOnly) | Some(TestContext::Doc)
477+
)
478+
{
479+
wp.suppressed = true;
480+
newly_suppressed += 1;
481+
continue;
482+
}
483+
484+
// (3) UnsafeFFI in BuildSystem/TestMock context.
485+
if matches!(wp.category, WeakPointCategory::UnsafeFFI) {
486+
let kind = FfiKind::classify_by_path(&file);
487+
if kind.is_audit_accepted_by_default() {
488+
wp.suppressed = true;
489+
newly_suppressed += 1;
490+
continue;
491+
}
492+
}
493+
}
494+
495+
newly_suppressed
496+
}
497+
396498
/// Build a fully-populated kanren FactDB from an assail report.
397499
///
398500
/// Ingest all facts (report, taint, cross-language, context) and run
@@ -650,3 +752,130 @@ mod classifications_tests {
650752
assert_eq!(report.suppressed_count, 1);
651753
}
652754
}
755+
756+
#[cfg(test)]
757+
mod v255_context_suppression_tests {
758+
use super::apply_v255_context_suppression;
759+
use crate::types::{
760+
AssailReport, AttackAxis, ProgramStatistics, Severity, TestContext,
761+
WeakPoint, WeakPointCategory,
762+
};
763+
use std::fs;
764+
use tempfile::TempDir;
765+
766+
fn report_with(wp: Vec<WeakPoint>) -> AssailReport {
767+
AssailReport {
768+
schema_version: "2.5".to_string(),
769+
program_path: std::path::PathBuf::from("test"),
770+
language: crate::types::Language::Rust,
771+
statistics: ProgramStatistics::default(),
772+
file_statistics: vec![],
773+
weak_points: wp,
774+
frameworks: vec![],
775+
recommended_attacks: vec![],
776+
dependency_graph: Default::default(),
777+
taint_matrix: Default::default(),
778+
migration_metrics: None,
779+
suppressed_count: 0,
780+
}
781+
}
782+
783+
fn wp(category: WeakPointCategory, location: &str) -> WeakPoint {
784+
WeakPoint {
785+
category,
786+
location: Some(location.to_string()),
787+
file: None,
788+
line: None,
789+
severity: Severity::Medium,
790+
description: "test".to_string(),
791+
recommended_attack: vec![AttackAxis::Memory],
792+
suppressed: false,
793+
test_context: None,
794+
}
795+
.with_parsed_location()
796+
}
797+
798+
#[test]
799+
fn panic_path_in_test_file_is_suppressed() {
800+
let mut report = report_with(vec![wp(
801+
WeakPointCategory::PanicPath,
802+
"tests/foo.rs:42",
803+
)]);
804+
let n = apply_v255_context_suppression(&mut report);
805+
assert_eq!(n, 1);
806+
assert!(report.weak_points[0].suppressed);
807+
assert_eq!(
808+
report.weak_points[0].test_context,
809+
Some(TestContext::TestOnly)
810+
);
811+
}
812+
813+
#[test]
814+
fn panic_path_in_prod_file_not_suppressed() {
815+
let mut report = report_with(vec![wp(
816+
WeakPointCategory::PanicPath,
817+
"src/main.rs:42",
818+
)]);
819+
let n = apply_v255_context_suppression(&mut report);
820+
assert_eq!(n, 0);
821+
assert!(!report.weak_points[0].suppressed);
822+
assert_eq!(
823+
report.weak_points[0].test_context,
824+
Some(TestContext::Production)
825+
);
826+
}
827+
828+
#[test]
829+
fn unsafe_ffi_in_build_zig_is_suppressed() {
830+
let mut report =
831+
report_with(vec![wp(WeakPointCategory::UnsafeFFI, "build.zig:5")]);
832+
let n = apply_v255_context_suppression(&mut report);
833+
assert_eq!(n, 1);
834+
assert!(report.weak_points[0].suppressed);
835+
}
836+
837+
#[test]
838+
fn unsafe_ffi_in_bindings_not_suppressed() {
839+
let mut report = report_with(vec![wp(
840+
WeakPointCategory::UnsafeFFI,
841+
"bindings/zig/cdef.zig:5",
842+
)]);
843+
let n = apply_v255_context_suppression(&mut report);
844+
assert_eq!(n, 0);
845+
assert!(!report.weak_points[0].suppressed);
846+
}
847+
848+
#[test]
849+
fn marker_suppression_via_filesystem() {
850+
let tmp = TempDir::new().unwrap();
851+
let path = tmp.path().join("foo.rs");
852+
// Line 2 has an unwrap; line 1 has the marker.
853+
fs::write(&path, "// panic-attack: accepted - test\nlet x = foo.unwrap();\n").unwrap();
854+
855+
let mut report = report_with(vec![wp(
856+
WeakPointCategory::UnsafeCode,
857+
&format!("{}:2", path.display()),
858+
)]);
859+
let n = apply_v255_context_suppression(&mut report);
860+
assert_eq!(n, 1);
861+
assert!(report.weak_points[0].suppressed);
862+
}
863+
864+
#[test]
865+
fn already_suppressed_finding_not_recounted() {
866+
let mut report = report_with(vec![WeakPoint {
867+
suppressed: true,
868+
..wp(WeakPointCategory::PanicPath, "tests/foo.rs:42")
869+
}]);
870+
let n = apply_v255_context_suppression(&mut report);
871+
// Already-suppressed findings are not counted as newly-suppressed,
872+
// but their test_context is still classified for downstream
873+
// consumers.
874+
assert_eq!(n, 0);
875+
assert!(report.weak_points[0].suppressed);
876+
assert_eq!(
877+
report.weak_points[0].test_context,
878+
Some(TestContext::TestOnly)
879+
);
880+
}
881+
}

src/main.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,12 @@ mod axial;
1919
#[cfg(feature = "http")]
2020
mod bridge;
2121
mod campaign;
22+
mod comment_marker;
2223
mod diagnostics;
24+
mod ffi_kind;
2325
mod groove;
2426
mod i18n;
27+
mod jit_context;
2528
mod kanren;
2629
mod kin;
2730
mod mass_panic;
@@ -32,6 +35,7 @@ mod report;
3235
mod signatures;
3336
mod storage;
3437
mod sweep_tracker;
38+
mod test_context;
3539
mod types;
3640

3741
extern crate walkdir;

0 commit comments

Comments
 (0)