use crate::{Finding, Result, Severity};
use regex::Regex;
use std::path::Path;
pub struct SlopsquattingDetector {
import_regex: Regex,
known_packages: Vec<(String, Vec<String>)>,
}
impl SlopsquattingDetector {
pub fn new() -> Self {
let import_regex = Regex::new(r#"import\s*(?:\(([^)]+)\)|"([^"]+)")"#).unwrap();
let known_packages = vec![
(
"github.com/hyperledger/fabric-chaincode-go".to_string(),
vec![
"github.com/hyperledger/fabric-chaincоde-go".to_string(), "github.com/hyperledger/fabric-chaincode-g0".to_string(), "github.com/hyperledger/fabric-chainc0de-go".to_string(), "github.com/hyperledger/fabrik-chaincode-go".to_string(), "github.com/hyperleger/fabric-chaincode-go".to_string(), "github.com/hyperledge/fabric-chaincode-go".to_string(), ],
),
(
"github.com/hyperledger/fabric-protos-go".to_string(),
vec![
"github.com/hyperledger/fabric-protоs-go".to_string(), "github.com/hyperledger/fabric-protos-g0".to_string(), "github.com/hyperledger/fabric-pr0tos-go".to_string(), ],
),
(
"github.com/golang/protobuf".to_string(),
vec![
"github.com/golang/protоbuf".to_string(), "github.com/golang/pr0tobuf".to_string(), "github.com/golang/protobuff".to_string(), "github.com/goland/protobuf".to_string(), ],
),
];
Self {
import_regex,
known_packages,
}
}
pub fn detect(&self, content: &str, path: &Path) -> Result<Vec<Finding>> {
let mut findings = Vec::new();
let imports = self.extract_imports(content);
for import in &imports {
for (legitimate, variations) in &self.known_packages {
if variations.contains(import) {
findings.push(Finding {
id: "SLOPSQUAT-KNOWN".to_string(),
severity: Severity::Critical,
category: "validation/slopsquatting".to_string(),
title: "Slopsquatting attack detected".to_string(),
description: format!(
"Import '{import}' is a malicious variation of legitimate package '{legitimate}'"
),
file: path.display().to_string(),
line: 0,
column: 0,
code_snippet: None,
remediation: Some(format!(
"Replace with legitimate package: {legitimate}"
)),
references: vec!["https://snyk.io/blog/typosquatting-attacks/".to_string()],
ai_consensus: None,
});
}
}
if self.contains_homographs(import) {
findings.push(Finding {
id: "SLOPSQUAT-HOMOGRAPH".to_string(),
severity: Severity::Critical,
category: "validation/slopsquatting".to_string(),
title: "Homograph attack detected".to_string(),
description: format!(
"Import '{import}' contains visually similar characters that may be malicious"
),
file: path.display().to_string(),
line: 0,
column: 0,
code_snippet: None,
remediation: Some("Verify the exact characters in the import path".to_string()),
references: vec![
"https://en.wikipedia.org/wiki/IDN_homograph_attack".to_string()
],
ai_consensus: None,
});
}
if self.is_suspicious_variation(import) {
findings.push(Finding {
id: "SLOPSQUAT-SUSPICIOUS".to_string(),
severity: Severity::High,
category: "validation/slopsquatting".to_string(),
title: "Suspicious package name variation".to_string(),
description: format!(
"Import '{import}' appears to be a variation of a known package"
),
file: path.display().to_string(),
line: 0,
column: 0,
code_snippet: None,
remediation: Some("Verify this is the intended package".to_string()),
references: vec![],
ai_consensus: None,
});
}
}
Ok(findings)
}
fn extract_imports(&self, content: &str) -> Vec<String> {
let mut imports = Vec::new();
for cap in self.import_regex.captures_iter(content) {
if let Some(multi_import) = cap.get(1) {
for line in multi_import.as_str().lines() {
let trimmed = line.trim();
if !trimmed.is_empty() && !trimmed.starts_with("//") {
let import = trimmed.trim_matches('"').to_string();
imports.push(import);
}
}
} else if let Some(single_import) = cap.get(2) {
imports.push(single_import.as_str().to_string());
}
}
imports
}
fn contains_homographs(&self, import: &str) -> bool {
let homographs = vec![
('а', 'a'), ('е', 'e'), ('о', 'o'), ('р', 'p'), ('с', 'c'), ('х', 'x'), ('у', 'y'), ('0', 'o'), ('1', 'l'), ('1', 'i'), ];
for (homograph, _) in homographs {
if import.contains(homograph) {
return true;
}
}
false
}
fn is_suspicious_variation(&self, import: &str) -> bool {
let suspicious_patterns = vec![
r"hyperledgerr",
r"fabricc",
r"chaincodee",
r"hyperldger",
r"fabrc",
r"chaincde",
r"hyperleger",
r"frabiс",
r"chanicоde",
r"hyper1edger",
r"fabr1c",
r"cha1ncode",
];
for pattern in suspicious_patterns {
if import.contains(pattern) {
return true;
}
}
for (legitimate, _) in &self.known_packages {
if self.levenshtein_distance(import, legitimate) <= 2 && import != legitimate {
return true;
}
}
false
}
fn levenshtein_distance(&self, s1: &str, s2: &str) -> usize {
let len1 = s1.chars().count();
let len2 = s2.chars().count();
let mut matrix = vec![vec![0; len2 + 1]; len1 + 1];
for i in 0..=len1 {
matrix[i][0] = i;
}
for j in 0..=len2 {
matrix[0][j] = j;
}
for (i, c1) in s1.chars().enumerate() {
for (j, c2) in s2.chars().enumerate() {
let cost = if c1 == c2 { 0 } else { 1 };
matrix[i + 1][j + 1] = (matrix[i][j + 1] + 1)
.min(matrix[i + 1][j] + 1)
.min(matrix[i][j] + cost);
}
}
matrix[len1][len2]
}
}