Skip to content

Commit 4994956

Browse files
authored
Merge pull request #3 from script3r/feat/cipherscope-scan-test
Feat/cipherscope scan test
2 parents baaadc8 + d010f67 commit 4994956

File tree

6 files changed

+316
-67
lines changed

6 files changed

+316
-67
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 & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +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>,
28-
2925
/// Number of threads
3026
#[arg(long, value_name = "N")]
3127
threads: Option<usize>,
@@ -42,22 +38,10 @@ struct Args {
4238
#[arg(long, value_name = "GLOB")]
4339
exclude_glob: Vec<String>,
4440

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-
5341
/// Deterministic output ordering
5442
#[arg(long, action = ArgAction::SetTrue)]
5543
deterministic: bool,
5644

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

157141
let mut cfg = Config {
158-
min_confidence: args.min_confidence,
159142
include_globs: args.include_glob.clone(),
160143
exclude_globs: args.exclude_glob.clone(),
161-
allow_libs: args.allow.clone(),
162-
deny_libs: args.deny.clone(),
163144
deterministic: args.deterministic,
164145
..Default::default()
165146
};
@@ -214,9 +195,6 @@ fn main() -> Result<()> {
214195
fs::write(sarif_path, serde_json::to_vec_pretty(&sarif)?)?;
215196
}
216197

217-
if args.fail_on_find && !findings.is_empty() {
218-
std::process::exit(2);
219-
}
220198
Ok(())
221199
}
222200

crates/cli/tests/anchors.rs

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

crates/cli/tests/filtering.rs

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

0 commit comments

Comments
 (0)