Skip to content

Commit 09a80b0

Browse files
committed
Require import+API; remove confidence and flags; tests + docs; clippy + fmt clean
1 parent baaadc8 commit 09a80b0

File tree

6 files changed

+296
-65
lines changed

6 files changed

+296
-65
lines changed

README.md

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,12 @@ JSONL and SARIF:
2121
```
2222

2323
Key flags:
24-
- `--min-confidence 0.9`: filter low-confidence hits
2524
- `--threads N`: set thread pool size
2625
- `--max-file-size MB`: skip large files (default 2)
2726
- `--patterns PATH`: specify patterns file (default: `patterns.toml`)
2827
- `--progress`: show progress bar during scanning
2928
- `--include-glob GLOB` / `--exclude-glob GLOB`
30-
- `--allow LIB` / `--deny LIB`
3129
- `--deterministic`: stable output ordering
32-
- `--fail-on-find`: exit 2 if findings exist
3330
- `--print-config`: print loaded `patterns.toml`
3431
- `--dry-run`: list files to be scanned
3532

@@ -48,7 +45,7 @@ Rust | RustCrypto | 2 | src/main.rs:12 aes_gcm::Aes256Gcm
4845
JSONL example:
4946

5047
```json
51-
{"language":"Rust","library":"RustCrypto","file":"src/main.rs","span":{"line":12,"column":5},"symbol":"aes_gcm::Aes256Gcm","snippet":"use aes_gcm::Aes256Gcm;","confidence":0.99,"detector_id":"detector-rust"}
48+
{"language":"Rust","library":"RustCrypto","file":"src/main.rs","span":{"line":12,"column":5},"symbol":"aes_gcm::Aes256Gcm","snippet":"use aes_gcm::Aes256Gcm;","detector_id":"detector-rust"}
5249
```
5350

5451
SARIF snippet:

crates/cli/src/main.rs

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,6 @@ struct Args {
2222
#[arg(long, value_name = "FILE")]
2323
sarif: Option<PathBuf>,
2424

25-
/// Minimum confidence required
26-
#[arg(long, value_name = "FLOAT")]
27-
min_confidence: Option<f32>,
2825

2926
/// Number of threads
3027
#[arg(long, value_name = "N")]
@@ -42,22 +39,10 @@ struct Args {
4239
#[arg(long, value_name = "GLOB")]
4340
exclude_glob: Vec<String>,
4441

45-
/// Allow only these libraries
46-
#[arg(long, value_name = "LIB")]
47-
allow: Vec<String>,
48-
49-
/// Deny these libraries
50-
#[arg(long, value_name = "LIB")]
51-
deny: Vec<String>,
52-
5342
/// Deterministic output ordering
5443
#[arg(long, action = ArgAction::SetTrue)]
5544
deterministic: bool,
5645

57-
/// Fail with code 2 if findings are present
58-
#[arg(long, action = ArgAction::SetTrue)]
59-
fail_on_find: bool,
60-
6146
/// Print merged patterns/config and exit
6247
#[arg(long, action = ArgAction::SetTrue)]
6348
print_config: bool,
@@ -155,11 +140,8 @@ fn main() -> Result<()> {
155140
];
156141

157142
let mut cfg = Config {
158-
min_confidence: args.min_confidence,
159143
include_globs: args.include_glob.clone(),
160144
exclude_globs: args.exclude_glob.clone(),
161-
allow_libs: args.allow.clone(),
162-
deny_libs: args.deny.clone(),
163145
deterministic: args.deterministic,
164146
..Default::default()
165147
};
@@ -214,9 +196,6 @@ fn main() -> Result<()> {
214196
fs::write(sarif_path, serde_json::to_vec_pretty(&sarif)?)?;
215197
}
216198

217-
if args.fail_on_find && !findings.is_empty() {
218-
std::process::exit(2);
219-
}
220199
Ok(())
221200
}
222201

crates/cli/tests/anchors.rs

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
use scanner_core::*;
2+
use std::fs;
3+
use std::path::{Path, PathBuf};
4+
use std::sync::Arc;
5+
use std::time::{SystemTime, UNIX_EPOCH};
6+
7+
fn write_file(dir: &Path, rel: &str, contents: &str) {
8+
let path = dir.join(rel);
9+
if let Some(parent) = path.parent() {
10+
fs::create_dir_all(parent).unwrap();
11+
}
12+
fs::write(path, contents).unwrap();
13+
}
14+
15+
fn tmp_dir(prefix: &str) -> PathBuf {
16+
let mut base = std::env::temp_dir();
17+
let ts = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos();
18+
let pid = std::process::id();
19+
base.push(format!("cipherscope_test_{}_{}_{}", prefix, pid, ts));
20+
fs::create_dir_all(&base).unwrap();
21+
base
22+
}
23+
24+
#[test]
25+
fn tink_requires_import_and_api() {
26+
let workspace = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
27+
let patterns_path = workspace.join("patterns.toml");
28+
let patterns = fs::read_to_string(patterns_path).unwrap();
29+
let reg = Arc::new(PatternRegistry::load(&patterns).unwrap());
30+
let dets: Vec<Box<dyn Detector>> = vec![Box::new(PatternDetector::new(
31+
"detector-java",
32+
&[Language::Java],
33+
reg.clone(),
34+
))];
35+
let scanner = Scanner::new(&reg, dets, Config::default());
36+
37+
// 1) Import only: should NOT report Tink
38+
let dir_import_only = tmp_dir("tink_import_only");
39+
write_file(
40+
&dir_import_only,
41+
"src/ImportOnly.java",
42+
r#"package test;
43+
import com.google.crypto.tink.aead.AeadConfig; // import present
44+
public class ImportOnly {
45+
public static void main(String[] args) { System.out.println("hello"); }
46+
}
47+
"#,
48+
);
49+
let findings = scanner.run(std::slice::from_ref(&dir_import_only)).unwrap();
50+
assert!(
51+
!findings
52+
.iter()
53+
.any(|f| f.library == "Google Tink (Java)"),
54+
"Tink should not be reported with import only"
55+
);
56+
57+
// 2) API only: should NOT report Tink
58+
let dir_api_only = tmp_dir("tink_api_only");
59+
write_file(
60+
&dir_api_only,
61+
"src/ApiOnly.java",
62+
r#"package test;
63+
public class ApiOnly {
64+
public static void main(String[] args) {
65+
// Mention API symbol without import
66+
String s = "Aead Mac HybridEncrypt"; // matches pattern by word, but no import
67+
System.out.println(s);
68+
}
69+
}
70+
"#,
71+
);
72+
let findings = scanner.run(std::slice::from_ref(&dir_api_only)).unwrap();
73+
assert!(
74+
!findings
75+
.iter()
76+
.any(|f| f.library == "Google Tink (Java)"),
77+
"Tink should not be reported with API mentions only"
78+
);
79+
80+
// 3) Import + API: should report Tink
81+
let dir_both = tmp_dir("tink_both");
82+
write_file(
83+
&dir_both,
84+
"src/Both.java",
85+
r#"package test;
86+
import com.google.crypto.tink.aead.AeadConfig; // import present
87+
public class Both {
88+
public static void main(String[] args) {
89+
// Include an API token
90+
String s = "Aead";
91+
System.out.println(s);
92+
}
93+
}
94+
"#,
95+
);
96+
let findings = scanner.run(std::slice::from_ref(&dir_both)).unwrap();
97+
assert!(
98+
findings
99+
.iter()
100+
.any(|f| f.library == "Google Tink (Java)"),
101+
"Tink should be reported when import and API are present"
102+
);
103+
}
104+

crates/cli/tests/filtering.rs

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
use scanner_core::*;
2+
use std::fs;
3+
use std::path::{Path, PathBuf};
4+
use std::sync::Arc;
5+
use std::time::{SystemTime, UNIX_EPOCH};
6+
7+
fn write_file(dir: &Path, rel: &str, contents: &str) {
8+
let path = dir.join(rel);
9+
if let Some(parent) = path.parent() {
10+
fs::create_dir_all(parent).unwrap();
11+
}
12+
fs::write(path, contents).unwrap();
13+
}
14+
15+
fn tmp_dir(prefix: &str) -> PathBuf {
16+
let mut base = std::env::temp_dir();
17+
let ts = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos();
18+
let pid = std::process::id();
19+
base.push(format!("cipherscope_test_{}_{}_{}", prefix, pid, ts));
20+
fs::create_dir_all(&base).unwrap();
21+
base
22+
}
23+
24+
fn load_registry() -> Arc<PatternRegistry> {
25+
let workspace = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
26+
let patterns_path = workspace.join("patterns.toml");
27+
let patterns = fs::read_to_string(patterns_path).unwrap();
28+
Arc::new(PatternRegistry::load(&patterns).unwrap())
29+
}
30+
31+
#[test]
32+
fn commented_import_does_not_trigger_anchor_java() {
33+
let reg = load_registry();
34+
let dets: Vec<Box<dyn Detector>> = vec![Box::new(PatternDetector::new(
35+
"detector-java",
36+
&[Language::Java],
37+
reg.clone(),
38+
))];
39+
let cfg = Config::default();
40+
let scanner = Scanner::new(&reg, dets, cfg);
41+
42+
let dir = tmp_dir("commented_import_java");
43+
write_file(
44+
&dir,
45+
"src/Main.java",
46+
r#"package test;
47+
// import javax.crypto.Cipher; // commented anchor
48+
public class Main {
49+
public static void main(String[] args) throws Exception {
50+
javax.crypto.Cipher.getInstance("AES/GCM/NoPadding"); // API present
51+
}
52+
}
53+
"#,
54+
);
55+
let findings = scanner.run(std::slice::from_ref(&dir)).unwrap();
56+
assert!(
57+
!findings
58+
.iter()
59+
.any(|f| f.library == "Java JCA/JCE"),
60+
"JCA/JCE should not be reported when import is commented"
61+
);
62+
}
63+
64+
#[test]
65+
fn php_api_only_reports_openssl() {
66+
let reg = load_registry();
67+
let dets: Vec<Box<dyn Detector>> = vec![Box::new(PatternDetector::new(
68+
"detector-php",
69+
&[Language::Php],
70+
reg.clone(),
71+
))];
72+
let cfg = Config::default();
73+
let scanner = Scanner::new(&reg, dets, cfg);
74+
75+
let dir = tmp_dir("php_openssl_api_only");
76+
write_file(
77+
&dir,
78+
"web/index.php",
79+
r#"<?php
80+
// No imports for PHP OpenSSL detector; API use is enough
81+
$ciphertext = openssl_encrypt("data", "aes-256-cbc", "key", 0, "1234567890123456");
82+
echo $ciphertext;
83+
"#,
84+
);
85+
let findings = scanner.run(std::slice::from_ref(&dir)).unwrap();
86+
assert!(
87+
findings.iter().any(|f| f.library == "OpenSSL (PHP)"),
88+
"OpenSSL (PHP) should be reported on API use only"
89+
);
90+
}
91+
92+
#[test]
93+
fn include_glob_filters_file_types() {
94+
let reg = load_registry();
95+
let dets_java: Vec<Box<dyn Detector>> = vec![
96+
Box::new(PatternDetector::new("detector-java", &[Language::Java], reg.clone())),
97+
Box::new(PatternDetector::new("detector-php", &[Language::Php], reg.clone())),
98+
];
99+
100+
let dir = tmp_dir("include_glob_filters");
101+
// Java file with anchor+API
102+
write_file(
103+
&dir,
104+
"src/Main.java",
105+
r#"package test;
106+
import java.security.MessageDigest;
107+
public class Main {
108+
public static void main(String[] args) throws Exception {
109+
java.security.KeyFactory.getInstance("RSA");
110+
}
111+
}
112+
"#,
113+
);
114+
// PHP file with API
115+
write_file(
116+
&dir,
117+
"web/index.php",
118+
r#"<?php
119+
echo openssl_encrypt("data", "aes-256-cbc", "key", 0, "1234567890123456");
120+
"#,
121+
);
122+
123+
// Only Java
124+
let cfg_java_only = Config {
125+
include_globs: vec!["**/*.java".to_string()],
126+
..Default::default()
127+
};
128+
let scanner_java = Scanner::new(&reg, dets_java, cfg_java_only);
129+
let findings_java = scanner_java.run(std::slice::from_ref(&dir)).unwrap();
130+
assert!(findings_java.iter().any(|f| f.library == "Java JCA/JCE"));
131+
assert!(
132+
!findings_java.iter().any(|f| f.library == "OpenSSL (PHP)"),
133+
"PHP findings should be excluded by include_glob"
134+
);
135+
136+
// Only PHP
137+
let cfg_php_only = Config {
138+
include_globs: vec!["**/*.php".to_string()],
139+
..Default::default()
140+
};
141+
let dets_php: Vec<Box<dyn Detector>> = vec![
142+
Box::new(PatternDetector::new("detector-java", &[Language::Java], reg.clone())),
143+
Box::new(PatternDetector::new("detector-php", &[Language::Php], reg.clone())),
144+
];
145+
let scanner_php = Scanner::new(&reg, dets_php, cfg_php_only);
146+
let findings_php = scanner_php.run(std::slice::from_ref(&dir)).unwrap();
147+
assert!(findings_php.iter().any(|f| f.library == "OpenSSL (PHP)"));
148+
assert!(
149+
!findings_php.iter().any(|f| f.library == "Java JCA/JCE"),
150+
"Java findings should be excluded by include_glob"
151+
);
152+
}
153+
154+
#[test]
155+
fn max_file_size_skips_large_files() {
156+
let reg = load_registry();
157+
let dets: Vec<Box<dyn Detector>> = vec![Box::new(PatternDetector::new(
158+
"detector-java",
159+
&[Language::Java],
160+
reg.clone(),
161+
))];
162+
163+
let dir = tmp_dir("max_file_size");
164+
// Create a large Java file that would otherwise match JCA
165+
let mut content = String::from(
166+
"package test;\nimport javax.crypto.Cipher;\npublic class Big { public static void main(String[] a){ } }\n",
167+
);
168+
// Append enough data to exceed threshold
169+
for _ in 0..5000 {
170+
content.push_str("// padding padding padding padding padding padding\n");
171+
}
172+
write_file(&dir, "src/Big.java", &content);
173+
174+
let cfg_small_limit = Config {
175+
max_file_size: 512, // bytes
176+
..Default::default()
177+
};
178+
let scanner = Scanner::new(&reg, dets, cfg_small_limit);
179+
let findings = scanner.run(std::slice::from_ref(&dir)).unwrap();
180+
assert!(findings.is_empty(), "Large file should be skipped by max_file_size");
181+
}
182+

0 commit comments

Comments
 (0)