#![allow(unused_imports)]
pub(crate) fn detector_relative_path(
repository_path: &std::path::Path,
path: &std::path::Path,
) -> std::path::PathBuf {
path.strip_prefix(repository_path)
.unwrap_or(path)
.to_path_buf()
}
macro_rules! detector_new {
($max:expr) => {
pub fn new(repository_path: impl Into<std::path::PathBuf>) -> Self {
Self {
repository_path: repository_path.into(),
max_findings: $max,
}
}
};
}
pub(crate) use detector_new;
macro_rules! impl_taint_precompute {
() => {
fn set_precomputed_taint(
&self,
cross: Vec<crate::detectors::security::taint::TaintPath>,
intra: Vec<crate::detectors::security::taint::TaintPath>,
) {
let _ = self.precomputed_cross.set(cross);
let _ = self.precomputed_intra.set(intra);
}
};
}
pub(crate) use impl_taint_precompute;
pub struct DetectorInit<'a> {
pub repo_path: &'a std::path::Path,
pub project_config: &'a crate::config::ProjectConfig,
pub resolver: crate::calibrate::ThresholdResolver,
pub ngram_model: Option<&'a crate::calibrate::NgramModel>,
}
impl<'a> DetectorInit<'a> {
pub fn config_for(&self, detector_name: &str) -> DetectorConfig {
DetectorConfig::from_project_config_with_type(
detector_name,
self.project_config,
self.repo_path,
)
.with_adaptive(self.resolver.clone())
}
#[cfg(test)]
pub fn test_default() -> DetectorInit<'static> {
let path: &'static std::path::Path =
Box::leak(std::env::current_dir().unwrap().into_boxed_path());
DetectorInit {
repo_path: path,
project_config: Box::leak(Box::new(crate::config::ProjectConfig::default())),
resolver: crate::calibrate::ThresholdResolver::default(),
ngram_model: None,
}
}
}
pub trait RegisteredDetector: Detector {
fn create(init: &DetectorInit) -> Arc<dyn Detector>
where
Self: Sized;
fn max_tier() -> crate::models::Tier
where
Self: Sized,
{
crate::models::Tier::Advisory
}
}
type DetectorFactory = fn(&DetectorInit) -> Arc<dyn Detector>;
const fn register<D: RegisteredDetector>() -> DetectorFactory {
D::create
}
pub const DEFAULT_DETECTOR_FACTORIES: &[DetectorFactory] = &[
register::<CircularDependencyDetector>(),
register::<SQLInjectionDetector>(),
register::<XssDetector>(),
register::<CommandInjectionDetector>(),
register::<SsrfDetector>(),
register::<PathTraversalDetector>(),
register::<SecretDetector>(),
register::<InsecureCryptoDetector>(),
register::<XxeDetector>(),
register::<PrototypePollutionDetector>(),
register::<InsecureTlsDetector>(),
register::<CleartextCredentialsDetector>(),
register::<NosqlInjectionDetector>(),
register::<LogInjectionDetector>(),
register::<InsecureDeserializeDetector>(),
register::<CorsMisconfigDetector>(),
register::<JwtWeakDetector>(),
register::<DjangoSecurityDetector>(),
register::<ExpressSecurityDetector>(),
register::<GHActionsInjectionDetector>(),
register::<EvalDetector>(),
register::<PickleDeserializationDetector>(),
register::<UnsafeTemplateDetector>(),
register::<InsecureCookieDetector>(),
register::<InsecureRandomDetector>(),
register::<RegexDosDetector>(),
register::<HardcodedIpsDetector>(),
register::<EmptyCatchDetector>(),
register::<BroadExceptionDetector>(),
register::<MutableDefaultArgsDetector>(),
register::<UnreachableCodeDetector>(),
register::<CallbackHellDetector>(),
register::<GeneratorMisuseDetector>(),
register::<InfiniteLoopDetector>(),
register::<NPlusOneDetector>(),
register::<RegexInLoopDetector>(),
register::<SyncInAsyncDetector>(),
register::<GodClassDetector>(),
register::<LongMethodsDetector>(),
register::<DeepNestingDetector>(),
register::<AIComplexitySpikeDetector>(),
register::<ShotgunSurgeryDetector>(),
register::<SinglePointOfFailureDetector>(),
register::<StructuralBridgeRiskDetector>(),
register::<MutualRecursionDetector>(),
register::<HiddenCouplingDetector>(),
register::<CommunityMisplacementDetector>(),
register::<PageRankDriftDetector>(),
register::<TemporalBottleneckDetector>(),
register::<ArchitecturalBottleneckDetector>(),
register::<DegreeCentralityDetector>(),
register::<UnwrapWithoutContextDetector>(),
register::<UnsafeWithoutSafetyCommentDetector>(),
register::<MutexPoisoningRiskDetector>(),
register::<PanicDensityDetector>(),
register::<TestInProductionDetector>(),
register::<DepAuditDetector>(),
register::<TorchLoadUnsafeDetector>(),
register::<NanEqualityDetector>(),
register::<MissingZeroGradDetector>(),
register::<ForwardMethodDetector>(),
register::<MissingRandomSeedDetector>(),
register::<ChainIndexingDetector>(),
register::<RequireGradTypoDetector>(),
register::<DeprecatedTorchApiDetector>(),
];
pub const DEEP_ONLY_DETECTOR_FACTORIES: &[DetectorFactory] = &[
register::<MissingAwaitDetector>(),
register::<UnhandledPromiseDetector>(),
register::<ReactHooksDetector>(),
register::<SingleOwnerModuleDetector>(),
register::<KnowledgeSiloDetector>(),
register::<OrphanedKnowledgeDetector>(),
register::<CriticalPathSingleOwnerDetector>(),
register::<ImplicitCoercionDetector>(),
register::<WildcardImportsDetector>(),
register::<GlobalVariablesDetector>(),
register::<StringConcatLoopDetector>(),
register::<HardcodedTimeoutDetector>(),
register::<CloneInHotPathDetector>(),
register::<MissingMustUseDetector>(),
register::<UnusedImportsDetector>(),
register::<LongParameterListDetector>(),
register::<DataClumpsDetector>(),
register::<LazyClassDetector>(),
register::<FeatureEnvyDetector>(),
register::<InappropriateIntimacyDetector>(),
register::<MessageChainDetector>(),
register::<MiddleManDetector>(),
register::<RefusedBequestDetector>(),
register::<ModuleCohesionDetector>(),
register::<DeadCodeDetector>(),
register::<DeadStoreDetector>(),
register::<DuplicateCodeDetector>(),
register::<AIDuplicateBlockDetector>(),
register::<AIMissingTestsDetector>(),
register::<AIBoilerplateDetector>(),
register::<AIChurnDetector>(),
register::<AINamingPatternDetector>(),
register::<TodoScanner>(),
register::<CommentedCodeDetector>(),
register::<SingleCharNamesDetector>(),
register::<DebugCodeDetector>(),
register::<MagicNumbersDetector>(),
register::<BooleanTrapDetector>(),
register::<InconsistentReturnsDetector>(),
register::<MissingDocstringsDetector>(),
register::<LargeFilesDetector>(),
register::<InfluentialCodeDetector>(),
register::<SurprisalDetector>(),
register::<HierarchicalSurprisalDetector>(),
register::<BoxDynTraitDetector>(),
];
pub fn create_default_detectors(init: &DetectorInit) -> Vec<Arc<dyn Detector>> {
DEFAULT_DETECTOR_FACTORIES
.iter()
.map(|f| f(init))
.filter(|d| init.project_config.is_detector_enabled(d.name()))
.collect()
}
pub fn create_all_detectors(init: &DetectorInit) -> Vec<Arc<dyn Detector>> {
DEFAULT_DETECTOR_FACTORIES
.iter()
.chain(DEEP_ONLY_DETECTOR_FACTORIES.iter())
.map(|f| f(init))
.filter(|d| init.project_config.is_detector_enabled(d.name()))
.collect()
}
pub mod ai;
pub mod architecture;
pub mod bugs;
pub mod performance;
pub mod quality;
pub mod security;
pub mod analysis_context;
pub mod api_surface;
pub mod ast_fingerprint;
pub(crate) mod ast_walk;
pub mod base;
pub mod class_context;
pub mod confidence_enrichment;
pub mod content_classifier;
pub mod context_hmm;
pub mod detector_context;
mod engine;
pub mod file_cache;
pub mod file_index;
pub mod file_provider;
pub mod framework_detection;
pub mod function_context;
pub mod graph_enrichment;
pub mod module_metrics;
pub mod reachability;
pub mod runner;
pub mod text_utils;
pub mod tiers;
pub mod user_input;
mod ml_smells;
mod rust_smells;
pub(crate) mod fast_search;
mod core_utility;
mod health_delta;
mod hierarchical_surprisal;
mod incremental_cache;
mod risk_analyzer;
mod root_cause_analyzer;
mod surprisal;
mod voting_engine;
pub use analysis_context::AnalysisContext;
pub use base::{
DetectionSummary, Detector, DetectorConfig, DetectorResult, DetectorScope, ProgressCallback,
};
pub use detector_context::{ContentFlags, DetectorContext};
pub use engine::{precompute_gd_startup, PrecomputedAnalysis, SerializablePrecomputed};
pub use file_cache::FileContentCache;
pub use file_index::{FileEntry, FileIndex};
pub use file_provider::{FileProvider, SourceFiles};
pub use function_context::{
FunctionContext, FunctionContextBuilder, FunctionContextMap, FunctionRole,
};
pub use runner::{
apply_hmm_context_filter, filter_test_file_findings, inject_taint_precomputed, run_detectors,
sort_findings_deterministic,
};
pub use tiers::{detector_max_tier, is_blocking_allowlisted, BLOCKING_ALLOWLIST};
pub use security::taint;
pub use security::{
CleartextCredentialsDetector, CommandInjectionDetector, CorsMisconfigDetector,
DepAuditDetector, DjangoSecurityDetector, EvalDetector, ExpressSecurityDetector,
GHActionsInjectionDetector, HardcodedIpsDetector, InsecureCookieDetector,
InsecureCryptoDetector, InsecureDeserializeDetector, InsecureRandomDetector,
InsecureTlsDetector, JwtWeakDetector, LogInjectionDetector, NosqlInjectionDetector,
PathTraversalDetector, PickleDeserializationDetector, PrototypePollutionDetector,
ReactHooksDetector, RegexDosDetector, SQLInjectionDetector, SecretDetector, SsrfDetector,
UnsafeTemplateDetector, XssDetector, XxeDetector,
};
pub use bugs::{
BroadExceptionDetector, CallbackHellDetector, EmptyCatchDetector, GeneratorMisuseDetector,
GlobalVariablesDetector, ImplicitCoercionDetector, InfiniteLoopDetector, MissingAwaitDetector,
MutableDefaultArgsDetector, StringConcatLoopDetector, UnhandledPromiseDetector,
UnreachableCodeDetector, WildcardImportsDetector,
};
pub use architecture::{
ArchitecturalBottleneckDetector, CircularDependencyDetector, CommunityMisplacementDetector,
CriticalPathSingleOwnerDetector, DegreeCentralityDetector, HiddenCouplingDetector,
KnowledgeSiloDetector, ModuleCohesionDetector, MutualRecursionDetector,
OrphanedKnowledgeDetector, PageRankDriftDetector, ShotgunSurgeryDetector,
SingleOwnerModuleDetector, SinglePointOfFailureDetector, StructuralBridgeRiskDetector,
TemporalBottleneckDetector,
};
pub use performance::{NPlusOneDetector, RegexInLoopDetector, SyncInAsyncDetector};
pub use quality::{
BooleanTrapDetector, CommentedCodeDetector, DataClumpsDetector, DeadCodeDetector,
DeadStoreDetector, DebugCodeDetector, DeepNestingDetector, DuplicateCodeDetector,
FeatureEnvyDetector, GodClassDetector, GodClassThresholds, HardcodedTimeoutDetector,
InappropriateIntimacyDetector, InconsistentReturnsDetector, InfluentialCodeDetector,
LargeFilesDetector, LazyClassDetector, LongMethodsDetector, LongParameterListDetector,
LongParameterThresholds, MagicNumbersDetector, MessageChainDetector, MiddleManDetector,
MissingDocstringsDetector, RefusedBequestDetector, SingleCharNamesDetector,
TestInProductionDetector, TodoScanner, UnusedImportsDetector,
};
pub use ai::{
AIBoilerplateDetector, AIChurnDetector, AIComplexitySpikeDetector, AIDuplicateBlockDetector,
AIMissingTestsDetector, AINamingPatternDetector, BoilerplatePattern,
};
pub use ml_smells::{
ChainIndexingDetector, DeprecatedTorchApiDetector, ForwardMethodDetector,
MissingRandomSeedDetector, MissingZeroGradDetector, NanEqualityDetector,
RequireGradTypoDetector, TorchLoadUnsafeDetector,
};
pub use rust_smells::{
BoxDynTraitDetector, CloneInHotPathDetector, MissingMustUseDetector,
MutexPoisoningRiskDetector, PanicDensityDetector, UnsafeWithoutSafetyCommentDetector,
UnwrapWithoutContextDetector,
};
pub use health_delta::{
estimate_batch_fix_impact, estimate_fix_impact, BatchHealthScoreDelta, HealthScoreDelta,
HealthScoreDeltaCalculator, ImpactLevel, MetricsBreakdown,
};
pub use incremental_cache::{
binary_file_hash, compute_fingerprint, prune_stale_caches, CacheStats, CachedScoreResult,
ConcurrentCacheView, IncrementalCache,
};
pub use risk_analyzer::{analyze_compound_risks, RiskAnalyzer, RiskAssessment, RiskFactor};
pub use root_cause_analyzer::{RootCauseAnalysis, RootCauseAnalyzer, RootCauseSummary};
pub use voting_engine::{
ConfidenceMethod, ConsensusResult, DetectorWeight, SeverityResolution, VotingEngine,
VotingStats, VotingStrategy,
};
pub use hierarchical_surprisal::HierarchicalSurprisalDetector;
pub use surprisal::SurprisalDetector;
use std::path::Path;
use std::sync::Arc;
pub fn build_threshold_resolver(
style_profile: Option<&crate::calibrate::StyleProfile>,
) -> crate::calibrate::ThresholdResolver {
crate::calibrate::ThresholdResolver::new(style_profile.cloned())
}
pub fn walk_source_files<'a>(
repository_path: &'a Path,
extensions: Option<&'a [&'a str]>,
) -> impl Iterator<Item = std::path::PathBuf> + 'a {
use ignore::WalkBuilder;
let mut builder = WalkBuilder::new(repository_path);
builder
.hidden(true)
.git_ignore(true)
.git_global(true)
.git_exclude(true)
.require_git(false)
.add_custom_ignore_filename(".repotoireignore");
builder.build().filter_map(move |entry| {
let entry = entry.ok()?;
let path = entry.path();
if !path.is_file() {
return None;
}
if let Some(exts) = extensions {
let ext = path.extension()?.to_str()?;
if !exts.contains(&ext) {
return None;
}
}
Some(path.to_path_buf())
})
}
pub fn is_line_suppressed(line: &str, prev_line: Option<&str>) -> bool {
let suppression_pattern = "repotoire: ignore";
let suppression_pattern_alt = "repotoire:ignore";
let line_lower = line.to_lowercase();
if line_lower.contains(suppression_pattern) || line_lower.contains(suppression_pattern_alt) {
return true;
}
if let Some(prev) = prev_line {
let prev_lower = prev.trim().to_lowercase();
if (prev_lower.starts_with('#')
|| prev_lower.starts_with("//")
|| prev_lower.starts_with("--")
|| prev_lower.starts_with("/*"))
&& (prev_lower.contains(suppression_pattern)
|| prev_lower.contains(suppression_pattern_alt))
{
return true;
}
}
false
}
#[allow(dead_code)]
pub fn is_line_suppressed_for(line: &str, prev_line: Option<&str>, detector_name: &str) -> bool {
suppression_kind_for_line(line, prev_line, detector_name).is_some()
}
pub fn is_file_suppressed(content: &str) -> bool {
is_file_suppressed_for(content, None)
}
pub fn is_file_suppressed_for(content: &str, detector_name: Option<&str>) -> bool {
match detector_name {
Some(name) => file_suppression_kind_for(content, name).is_some(),
None => is_file_suppressed_bare(content),
}
}
fn is_file_suppressed_bare(content: &str) -> bool {
for line in content.lines().take(10) {
let lower = line.to_lowercase();
let trimmed = lower.trim();
let is_comment = trimmed.starts_with('#')
|| trimmed.starts_with("//")
|| trimmed.starts_with("/*")
|| trimmed.starts_with("--")
|| trimmed.starts_with('*');
if !is_comment {
continue;
}
for pat in &["repotoire:ignore-file", "repotoire: ignore-file"] {
if let Some(idx) = lower.find(pat) {
let rest = &lower[idx + pat.len()..];
if !rest.starts_with('[') {
return true;
}
}
}
}
false
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Reason {
FrameworkPattern,
TestFixture,
ProtocolRequired,
RedactionList,
Vendored,
Generated,
AcceptedRisk,
}
impl Reason {
pub fn parse(s: &str) -> Option<Reason> {
match s.trim().to_lowercase().replace('_', "-").as_str() {
"framework-pattern" => Some(Reason::FrameworkPattern),
"test-fixture" => Some(Reason::TestFixture),
"protocol-required" => Some(Reason::ProtocolRequired),
"redaction-list" => Some(Reason::RedactionList),
"vendored" => Some(Reason::Vendored),
"generated" => Some(Reason::Generated),
"accepted-risk" => Some(Reason::AcceptedRisk),
_ => None,
}
}
pub fn as_str(&self) -> &'static str {
match self {
Reason::FrameworkPattern => "framework-pattern",
Reason::TestFixture => "test-fixture",
Reason::ProtocolRequired => "protocol-required",
Reason::RedactionList => "redaction-list",
Reason::Vendored => "vendored",
Reason::Generated => "generated",
Reason::AcceptedRisk => "accepted-risk",
}
}
pub fn harvestable(&self) -> bool {
!matches!(self, Reason::AcceptedRisk)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SuppressionKind {
DisplayOnly,
Accounted(Reason),
}
fn parse_reason_suffix(after_bracket: &str) -> SuppressionKind {
let trimmed = after_bracket.trim_start();
let rest = if let Some(r) = trimmed.strip_prefix('\u{2014}') {
r
} else if let Some(r) = trimmed.strip_prefix("--") {
r
} else if let Some(r) = trimmed.strip_prefix(':') {
r
} else {
return SuppressionKind::DisplayOnly;
};
match Reason::parse(rest) {
Some(reason) => SuppressionKind::Accounted(reason),
None => SuppressionKind::DisplayOnly,
}
}
fn check_suppression_kind(text: &str, detector_name: &str) -> Option<SuppressionKind> {
let lower = text.to_lowercase();
let det_norm = normalize_detector_id(detector_name);
for prefix in &["repotoire:ignore", "repotoire: ignore"] {
if let Some(idx) = lower.find(prefix) {
let after = idx + prefix.len();
let rest_lower = &lower[after..];
let rest_orig = &text[after..];
if rest_lower.starts_with('[') {
if let Some(end) = rest_lower.find(']') {
let target = &rest_lower[1..end];
if normalize_detector_id(target.trim()) != det_norm {
continue;
}
let after_bracket = &rest_orig[end + 1..];
return Some(parse_reason_suffix(after_bracket));
}
} else {
return Some(SuppressionKind::DisplayOnly);
}
}
}
None
}
pub fn suppression_kind_for_line(
line: &str,
prev_line: Option<&str>,
detector_name: &str,
) -> Option<SuppressionKind> {
if let Some(kind) = check_suppression_kind(line, detector_name) {
return Some(kind);
}
if let Some(prev) = prev_line {
let prev_lower = prev.trim().to_lowercase();
if prev_lower.starts_with('#')
|| prev_lower.starts_with("//")
|| prev_lower.starts_with("--")
|| prev_lower.starts_with("/*")
{
if let Some(kind) = check_suppression_kind(prev, detector_name) {
return Some(kind);
}
}
}
None
}
pub fn file_suppression_kind_for(content: &str, detector_name: &str) -> Option<SuppressionKind> {
let det_norm = normalize_detector_id(detector_name);
for line in content.lines().take(10) {
let lower = line.to_lowercase();
let trimmed_lower = lower.trim();
let is_comment = trimmed_lower.starts_with('#')
|| trimmed_lower.starts_with("//")
|| trimmed_lower.starts_with("/*")
|| trimmed_lower.starts_with("--")
|| trimmed_lower.starts_with('*');
if !is_comment {
continue;
}
for pat in &["repotoire:ignore-file", "repotoire: ignore-file"] {
if let Some(idx) = lower.find(pat) {
let after = idx + pat.len();
let rest_lower = &lower[after..];
let rest_orig = &line[after..];
if rest_lower.starts_with('[') {
if let Some(end) = rest_lower.find(']') {
let target = &rest_lower[1..end];
if normalize_detector_id(target.trim()) == det_norm {
let after_bracket = &rest_orig[end + 1..];
return Some(parse_reason_suffix(after_bracket));
}
}
} else {
return Some(SuppressionKind::DisplayOnly);
}
}
}
}
None
}
pub fn normalize_detector_id(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
if c.is_ascii_alphanumeric() {
out.push(c.to_ascii_lowercase());
}
}
if out.ends_with("detector") && out.len() > "detector".len() {
out.truncate(out.len() - "detector".len());
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn deep_set_detectors_declare_deep_tier() {
let init = DetectorInit::test_default();
let deep: std::collections::HashSet<String> = DEEP_ONLY_DETECTOR_FACTORIES
.iter()
.map(|f| f(&init).name().to_string())
.collect();
for name in &deep {
assert!(
!is_blocking_allowlisted(name),
"{name} is both deep-only and blocking-allowlisted"
);
}
}
#[test]
fn test_inline_suppression() {
assert!(is_line_suppressed("x = 1 // repotoire:ignore", None));
assert!(is_line_suppressed("x = 1 // repotoire: ignore", None));
}
#[test]
fn test_prev_line_suppression() {
assert!(is_line_suppressed("x = 1", Some("// repotoire:ignore")));
}
#[test]
fn test_no_suppression() {
assert!(!is_line_suppressed("x = 1", None));
assert!(!is_line_suppressed("x = 1", Some("// normal comment")));
}
#[test]
fn test_targeted_suppression() {
assert!(is_line_suppressed_for(
"x = 1 // repotoire:ignore[sql-injection]",
None,
"sql-injection"
));
assert!(!is_line_suppressed_for(
"x = 1 // repotoire:ignore[sql-injection]",
None,
"xss"
));
assert!(is_line_suppressed_for(
"x = 1 // repotoire:ignore",
None,
"xss"
));
assert!(is_line_suppressed_for(
"x = 1",
Some("// repotoire:ignore[xss]"),
"xss"
));
assert!(!is_line_suppressed_for(
"x = 1",
Some("// repotoire:ignore[xss]"),
"sql-injection"
));
}
#[test]
fn test_targeted_suppression_with_space() {
assert!(is_line_suppressed_for(
"x = 1 // repotoire: ignore[sql-injection]",
None,
"sql-injection"
));
assert!(!is_line_suppressed_for(
"x = 1 // repotoire: ignore[sql-injection]",
None,
"xss"
));
}
#[test]
fn test_targeted_suppression_case_insensitive() {
assert!(is_line_suppressed_for(
"x = 1 // Repotoire:Ignore[SQL-Injection]",
None,
"sql-injection"
));
}
#[test]
fn test_targeted_suppression_bare_prev_line() {
assert!(is_line_suppressed_for(
"x = 1",
Some("// repotoire:ignore"),
"any-detector"
));
}
#[test]
fn test_normalize_detector_id() {
assert_eq!(
normalize_detector_id("InsecureCryptoDetector"),
"insecurecrypto"
);
assert_eq!(normalize_detector_id("insecure-crypto"), "insecurecrypto");
assert_eq!(normalize_detector_id("insecure_crypto"), "insecurecrypto");
assert_eq!(normalize_detector_id("InsecureCrypto"), "insecurecrypto");
assert_eq!(normalize_detector_id("insecurecrypto"), "insecurecrypto");
assert_eq!(
normalize_detector_id(" Insecure-Crypto Detector "),
"insecurecrypto"
);
assert_eq!(normalize_detector_id("Detector"), "detector");
assert_eq!(normalize_detector_id(""), "");
}
#[test]
fn test_targeted_suppression_matches_struct_name_against_kebab_id() {
assert!(is_line_suppressed_for(
"let h = sha1(data); // repotoire:ignore[insecure-crypto]",
None,
"InsecureCryptoDetector"
));
assert!(is_line_suppressed_for(
"let h = sha1(data); // repotoire:ignore[InsecureCryptoDetector]",
None,
"insecure-crypto"
));
assert!(!is_line_suppressed_for(
"let h = sha1(data); // repotoire:ignore[insecure-crypto]",
None,
"SqlInjectionDetector"
));
}
#[test]
fn test_file_suppression_untargeted() {
let content =
"//! Pure-Rust SHA1 implementation.\n//! repotoire:ignore-file\nfn sha1() {}\n";
assert!(is_file_suppressed(content));
assert!(is_file_suppressed_for(
content,
Some("InsecureCryptoDetector")
));
assert!(is_file_suppressed_for(content, Some("AnyOtherDetector")));
}
#[test]
fn test_file_suppression_targeted() {
let content = "//! repotoire:ignore-file[insecure-crypto]\nfn sha1() {}\n";
assert!(is_file_suppressed_for(
content,
Some("InsecureCryptoDetector")
));
assert!(is_file_suppressed_for(content, Some("insecure-crypto")));
assert!(is_file_suppressed_for(content, Some("Insecure_Crypto")));
assert!(!is_file_suppressed_for(
content,
Some("SqlInjectionDetector")
));
assert!(!is_file_suppressed_for(content, None));
assert!(!is_file_suppressed(content));
}
#[test]
fn test_file_suppression_after_other_comments() {
let content = r#"//! RFC 3174 SHA-1 implementation (hash-only, not for cryptographic use).
//! Used exclusively for git object ID computation.
//!
//! repotoire:ignore-file[insecure-crypto]
//! SHA-1 is mandated by the Git protocol.
const H0: u32 = 0x67452301;
"#;
assert!(is_file_suppressed_for(
content,
Some("InsecureCryptoDetector")
));
assert!(!is_file_suppressed_for(content, Some("DebugCodeDetector")));
}
#[test]
fn test_targeted_suppression_prev_line_non_comment() {
assert!(!is_line_suppressed_for(
"x = 1",
Some("x = 1 repotoire:ignore[xss]"),
"xss"
));
}
#[test]
fn test_targeted_suppression_python_comment() {
assert!(is_line_suppressed_for(
"x = 1 # repotoire:ignore[magic-numbers]",
None,
"magic-numbers"
));
}
#[test]
fn test_targeted_suppression_no_match_no_suppress() {
assert!(!is_line_suppressed_for("x = 1", None, "sql-injection"));
assert!(!is_line_suppressed_for(
"x = 1",
Some("// normal comment"),
"sql-injection"
));
}
#[test]
fn all_detectors_have_scope() {
let init = DetectorInit::test_default();
let detectors = create_all_detectors(&init);
for d in &detectors {
let scope = d.detector_scope();
match scope {
DetectorScope::FileLocal
| DetectorScope::FileScopedGraph
| DetectorScope::GraphWide => {}
}
}
let file_local = detectors
.iter()
.filter(|d| d.detector_scope() == DetectorScope::FileLocal)
.count();
let graph_wide = detectors
.iter()
.filter(|d| d.detector_scope() == DetectorScope::GraphWide)
.count();
assert!(
file_local > 20,
"Expected 20+ FileLocal detectors, got {}",
file_local
);
assert!(
graph_wide >= 2,
"Expected 2+ GraphWide detectors, got {}",
graph_wide
);
}
#[test]
fn test_file_suppressed_rust_comment() {
assert!(is_file_suppressed("// repotoire:ignore-file\nfn main() {}"));
}
#[test]
fn test_file_suppressed_rust_comment_with_space() {
assert!(is_file_suppressed(
"// repotoire: ignore-file\nfn main() {}"
));
}
#[test]
fn test_file_suppressed_python_comment() {
assert!(is_file_suppressed("# repotoire:ignore-file\nimport os"));
}
#[test]
fn test_file_suppressed_c_style_comment() {
assert!(is_file_suppressed(
"/* repotoire:ignore-file */\nint main() {}"
));
}
#[test]
fn test_file_suppressed_sql_comment() {
assert!(is_file_suppressed("-- repotoire:ignore-file\nSELECT 1;"));
}
#[test]
fn test_file_suppressed_block_comment_continuation() {
assert!(is_file_suppressed(
"/*\n * repotoire:ignore-file\n */\nfn main() {}"
));
}
#[test]
fn test_file_suppressed_only_first_10_lines() {
let mut lines: Vec<&str> = vec!["// normal comment"; 10];
lines.push("// repotoire:ignore-file");
assert!(!is_file_suppressed(&lines.join("\n")));
}
#[test]
fn test_file_suppressed_within_10_lines() {
let mut lines: Vec<&str> = vec!["// normal comment"; 9];
lines.push("// repotoire:ignore-file");
assert!(is_file_suppressed(&lines.join("\n")));
}
#[test]
fn test_file_not_suppressed_plain_code() {
assert!(!is_file_suppressed(
"fn main() {\n println!(\"hello\");\n}"
));
}
#[test]
fn test_file_not_suppressed_in_code_line() {
assert!(!is_file_suppressed(
"let x = \"repotoire:ignore-file\";\nfn main() {}"
));
}
#[test]
fn test_file_suppressed_case_insensitive() {
assert!(is_file_suppressed("// Repotoire:Ignore-File\nfn main() {}"));
}
#[test]
fn test_file_suppressed_for_targeted() {
assert!(is_file_suppressed_for(
"// repotoire:ignore-file[sql-injection]\nfn main() {}",
Some("sql-injection")
));
}
#[test]
fn test_file_suppressed_for_targeted_no_match() {
assert!(!is_file_suppressed_for(
"// repotoire:ignore-file[sql-injection]\nfn main() {}",
Some("xss")
));
}
#[test]
fn test_file_suppressed_for_blanket() {
assert!(is_file_suppressed_for(
"// repotoire:ignore-file\nfn main() {}",
Some("any-detector")
));
}
#[test]
fn test_file_suppressed_for_targeted_does_not_match_blanket_check() {
assert!(!is_file_suppressed_for(
"// repotoire:ignore-file[sql-injection]\nfn main() {}",
None
));
}
#[test]
fn test_create_all_detectors_registry() {
let init = DetectorInit::test_default();
let default = create_default_detectors(&init);
let all = create_all_detectors(&init);
assert!(default.len() > 50, "default detectors: {}", default.len());
assert!(
all.len() > default.len(),
"all ({}) should exceed default ({})",
all.len(),
default.len()
);
assert_eq!(all.len(), 110);
}
#[test]
fn test_detector_init_config_for() {
let init = DetectorInit::test_default();
let config = init.config_for("GodClassDetector");
assert!(config.coupling_multiplier > 0.0);
}
#[test]
fn reason_taxonomy_parsing() {
use SuppressionKind::*;
assert!(matches!(
suppression_kind_for_line(
"x // repotoire:ignore[sql-injection] \u{2014} accepted-risk",
None,
"sql-injection"
),
Some(Accounted(Reason::AcceptedRisk))
));
assert!(matches!(
suppression_kind_for_line(
"x # repotoire:ignore[secret] -- redaction-list",
None,
"secret"
),
Some(Accounted(Reason::RedactionList))
));
assert!(matches!(
suppression_kind_for_line("x // repotoire:ignore[xss]: framework-pattern", None, "xss"),
Some(Accounted(Reason::FrameworkPattern))
));
assert!(matches!(
suppression_kind_for_line("x // repotoire:ignore", None, "xss"),
Some(DisplayOnly)
));
assert!(matches!(
suppression_kind_for_line("x // repotoire:ignore[xss]", None, "xss"),
Some(DisplayOnly)
));
assert!(matches!(
suppression_kind_for_line(
"x // repotoire:ignore[xss] \u{2014} because reasons",
None,
"xss"
),
Some(DisplayOnly)
));
assert!(suppression_kind_for_line(
"x // repotoire:ignore[xss] \u{2014} accepted-risk",
None,
"sql-injection"
)
.is_none());
}
}