Skip to content

Commit 395ca10

Browse files
committed
Erlang
1 parent 1be357c commit 395ca10

File tree

14 files changed

+528
-457
lines changed

14 files changed

+528
-457
lines changed

Cargo.lock

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ members = [
1111
"crates/detector-swift",
1212
"crates/detector-objc",
1313
"crates/detector-kotlin",
14+
"crates/detector-erlang",
1415
"crates/cli",
1516
]
1617
resolver = "2"

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
## CipherScope
22

3-
Fast, low-false-positive static scanner that finds third-party cryptographic libraries and call sites across 10 programming languages: Go, Java, C, C++, Rust, Python, PHP, Swift, Objective-C, and Kotlin.
3+
Fast, low-false-positive static scanner that finds third-party cryptographic libraries and call sites across 11 programming languages: Go, Java, C, C++, Rust, Python, PHP, Swift, Objective-C, Kotlin, and Erlang.
44

55
### Install & Run
66

@@ -70,6 +70,7 @@ The scanner automatically detects and processes files with these extensions:
7070
- **Swift**: `.swift`
7171
- **Objective-C**: `.m`, `.mm`, `.M`
7272
- **Kotlin**: `.kt`, `.kts`
73+
- **Erlang**: `.erl`, `.hrl`, `.beam`
7374

7475
#### Performance Optimizations
7576

@@ -92,6 +93,7 @@ The scanner uses a modular detector architecture with dedicated crates for each
9293
- **detector-swift**: Swift language support
9394
- **detector-objc**: Objective-C language support
9495
- **detector-kotlin**: Kotlin language support
96+
- **detector-erlang**: Erlang language support
9597

9698
Each detector implements the `Detector` trait and can be extended independently. To add support for a new language, create a new detector crate under `crates/` or extend the `patterns.toml` to cover additional libraries. See `crates/scanner-core/src/lib.rs` for the trait definition and pattern-driven detector implementation.
9799

crates/cli/src/main.rs

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -147,18 +147,25 @@ fn main() -> Result<()> {
147147
&[Language::Kotlin],
148148
reg.clone(),
149149
)),
150+
Box::new(PatternDetector::new(
151+
"detector-erlang",
152+
&[Language::Erlang],
153+
reg.clone(),
154+
)),
150155
];
151156

152-
let mut cfg = Config::default();
153-
cfg.min_confidence = args.min_confidence;
157+
let mut cfg = Config {
158+
min_confidence: args.min_confidence,
159+
include_globs: args.include_glob.clone(),
160+
exclude_globs: args.exclude_glob.clone(),
161+
allow_libs: args.allow.clone(),
162+
deny_libs: args.deny.clone(),
163+
deterministic: args.deterministic,
164+
..Default::default()
165+
};
154166
if let Some(mb) = args.max_file_size {
155167
cfg.max_file_size = mb * 1024 * 1024;
156168
}
157-
cfg.include_globs = args.include_glob.clone();
158-
cfg.exclude_globs = args.exclude_glob.clone();
159-
cfg.allow_libs = args.allow.clone();
160-
cfg.deny_libs = args.deny.clone();
161-
cfg.deterministic = args.deterministic;
162169

163170
// Set up progress reporting if requested
164171
if args.progress {

crates/cli/tests/integration.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ fn scan_fixtures() {
4848
];
4949
let scanner = Scanner::new(&reg, dets, Config::default());
5050
let fixtures = workspace.join("fixtures");
51-
let findings = scanner.run(&[fixtures.clone()]).unwrap();
51+
let findings = scanner.run(std::slice::from_ref(&fixtures)).unwrap();
5252

5353
// Debug: print all findings
5454
println!("Found {} findings:", findings.len());

crates/detector-erlang/Cargo.toml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[package]
2+
name = "detector-erlang"
3+
version.workspace = true
4+
edition.workspace = true
5+
license.workspace = true
6+
authors.workspace = true
7+
description = "Erlang cryptographic library detector for CipherScope"
8+
9+
[dependencies]
10+
scanner-core = { path = "../scanner-core" }
11+
anyhow = "1"

crates/detector-erlang/src/lib.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
use scanner_core::{Detector, Language, ScanUnit, Emitter, Prefilter};
2+
use anyhow::Result;
3+
use std::collections::BTreeSet;
4+
5+
pub struct ErlangDetector;
6+
7+
impl Detector for ErlangDetector {
8+
fn id(&self) -> &'static str {
9+
"detector-erlang"
10+
}
11+
12+
fn languages(&self) -> &'static [Language] {
13+
&[Language::Erlang]
14+
}
15+
16+
fn prefilter(&self) -> Prefilter {
17+
Prefilter {
18+
extensions: BTreeSet::from([".erl".to_string(), ".hrl".to_string(), ".beam".to_string()]),
19+
substrings: BTreeSet::new(),
20+
}
21+
}
22+
23+
fn scan(&self, _unit: &ScanUnit, _em: &mut Emitter) -> Result<()> {
24+
// This detector is not used since we use PatternDetector instead
25+
Ok(())
26+
}
27+
28+
fn as_any(&self) -> &dyn std::any::Any {
29+
self
30+
}
31+
}

crates/scanner-core/benches/throughput.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ fn bench_scan(c: &mut Criterion) {
5151
.throughput(Throughput::Bytes(10_000_000))
5252
.bench_function("fixtures", |b| {
5353
b.iter(|| {
54-
let _ = scanner.run(&[root.clone()]).unwrap();
54+
let _ = scanner.run(std::slice::from_ref(&root)).unwrap();
5555
});
5656
});
5757
}

crates/scanner-core/src/lib.rs

Lines changed: 39 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ use std::sync::Mutex;
1414

1515
// ---------------- Types ----------------
1616

17+
type ProgressCallback = Arc<dyn Fn(usize, usize, usize) + Send + Sync>;
18+
1719
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
1820
pub enum Language {
1921
Go,
@@ -26,6 +28,7 @@ pub enum Language {
2628
Swift,
2729
ObjC,
2830
Kotlin,
31+
Erlang,
2932
}
3033

3134
impl<'de> Deserialize<'de> for Language {
@@ -47,6 +50,7 @@ impl<'de> Deserialize<'de> for Language {
4750
"swift" => Ok(Language::Swift),
4851
"objc" | "objective-c" | "objectivec" => Ok(Language::ObjC),
4952
"kotlin" | "kt" => Ok(Language::Kotlin),
53+
"erlang" | "erl" => Ok(Language::Erlang),
5054
other => Err(D::Error::invalid_value(
5155
Unexpected::Str(other),
5256
&"valid language",
@@ -129,7 +133,7 @@ impl Emitter {
129133
self.rx.try_iter().collect()
130134
}
131135

132-
pub fn into_iter(self) -> Receiver<Finding> {
136+
pub fn into_receiver(self) -> Receiver<Finding> {
133137
self.rx
134138
}
135139
}
@@ -186,7 +190,7 @@ pub struct Config {
186190
#[serde(default)]
187191
pub deterministic: bool,
188192
#[serde(skip)]
189-
pub progress_callback: Option<Arc<dyn Fn(usize, usize, usize) + Send + Sync>>,
193+
pub progress_callback: Option<ProgressCallback>,
190194
}
191195

192196
fn default_max_file_size() -> usize {
@@ -277,6 +281,10 @@ fn default_include_globs() -> Vec<String> {
277281
// Kotlin
278282
"**/*.kt".to_string(),
279283
"**/*.kts".to_string(),
284+
// Erlang
285+
"**/*.erl".to_string(),
286+
"**/*.hrl".to_string(),
287+
"**/*.beam".to_string(),
280288
]
281289
}
282290

@@ -305,7 +313,7 @@ impl PatternRegistry {
305313
let libs = pf
306314
.library
307315
.into_iter()
308-
.map(|lib| compile_library(lib))
316+
.map(compile_library)
309317
.collect::<Result<Vec<_>>>()?;
310318

311319
// Build language cache only if we have many libraries
@@ -409,7 +417,8 @@ mod strip {
409417
| Language::Rust
410418
| Language::Swift
411419
| Language::ObjC
412-
| Language::Kotlin => strip_c_like(language, input),
420+
| Language::Kotlin
421+
| Language::Erlang => strip_c_like(language, input),
413422
Language::Python | Language::Php => strip_hash_like(language, input),
414423
}
415424
}
@@ -746,10 +755,7 @@ impl<'a> Scanner<'a> {
746755
}
747756
}
748757
}
749-
match builder.build() {
750-
Ok(matcher) => Some(matcher),
751-
Err(_) => None,
752-
}
758+
builder.build().ok()
753759
} else {
754760
None
755761
};
@@ -762,28 +768,26 @@ impl<'a> Scanner<'a> {
762768
.git_exclude(true)
763769
.ignore(true);
764770

765-
for result in builder.build() {
766-
if let Ok(entry) = result {
767-
let md = match entry.metadata() {
768-
Ok(m) => m,
769-
Err(_) => continue,
770-
};
771-
if md.is_file() {
772-
if md.len() as usize > self.config.max_file_size {
773-
continue;
774-
}
771+
for entry in builder.build().flatten() {
772+
let md = match entry.metadata() {
773+
Ok(m) => m,
774+
Err(_) => continue,
775+
};
776+
if md.is_file() {
777+
if md.len() as usize > self.config.max_file_size {
778+
continue;
779+
}
775780

776-
let path = entry.into_path();
781+
let path = entry.into_path();
777782

778-
// Apply include glob filtering
779-
if let Some(ref matcher) = include_matcher {
780-
if !matcher.is_match(&path) {
781-
continue;
782-
}
783+
// Apply include glob filtering
784+
if let Some(ref matcher) = include_matcher {
785+
if !matcher.is_match(&path) {
786+
continue;
783787
}
784-
785-
paths.push(path);
786788
}
789+
790+
paths.push(path);
787791
}
788792
}
789793
}
@@ -811,6 +815,7 @@ impl<'a> Scanner<'a> {
811815
"swift" => Some(Language::Swift),
812816
"m" | "mm" | "M" => Some(Language::ObjC),
813817
"kt" | "kts" => Some(Language::Kotlin),
818+
"erl" | "hrl" | "beam" => Some(Language::Erlang),
814819
_ => None,
815820
}
816821
}
@@ -842,7 +847,7 @@ impl<'a> Scanner<'a> {
842847
let mut processed = 0;
843848
let findings_count = 0;
844849

845-
while let Ok(_) = progress_rx.recv() {
850+
while progress_rx.recv().is_ok() {
846851
processed += 1;
847852
callback(processed, total_files, findings_count);
848853
}
@@ -874,7 +879,7 @@ impl<'a> Scanner<'a> {
874879
if !det.languages().contains(&lang) {
875880
continue;
876881
}
877-
if !prefilter_hit(det, &stripped) {
882+
if !prefilter_hit(det.as_ref(), &stripped) {
878883
continue;
879884
}
880885
let _ = det.scan_optimized(&unit, &stripped_s, &index, &mut em);
@@ -936,7 +941,7 @@ impl<'a> Scanner<'a> {
936941
}
937942
}
938943

939-
fn prefilter_hit(det: &Box<dyn Detector>, stripped: &[u8]) -> bool {
944+
fn prefilter_hit(det: &dyn Detector, stripped: &[u8]) -> bool {
940945
let pf = det.prefilter();
941946
if pf.substrings.is_empty() {
942947
return true;
@@ -1051,7 +1056,7 @@ impl PatternDetector {
10511056
}
10521057
}
10531058
let should_report =
1054-
(matched_import && api_hits > 0) || (lib.import.is_empty() && api_hits > 0);
1059+
(lib.import.is_empty() || matched_import) && api_hits > 0;
10551060
if should_report {
10561061
let finding = Finding {
10571062
language: unit.lang,
@@ -1089,14 +1094,12 @@ impl Detector for PatternDetector {
10891094
substrings.insert(s.clone());
10901095
}
10911096
}
1092-
let pf = Prefilter {
1093-
extensions: BTreeSet::new(),
1094-
substrings,
1095-
};
1096-
10971097
// Note: We can't actually cache here due to &self, but this is still faster
10981098
// than recomputing every time since we're using the cached language lookup
1099-
pf
1099+
Prefilter {
1100+
extensions: BTreeSet::new(),
1101+
substrings,
1102+
}
11001103
}
11011104
fn scan(&self, unit: &ScanUnit, em: &mut Emitter) -> Result<()> {
11021105
let libs = self.registry.for_language(unit.lang);

fixtures/erlang/positive/main.erl

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
-module(main).
2+
-export([main/0]).
3+
4+
-include_lib("public_key/include/public_key.hrl").
5+
6+
main() ->
7+
% Test Erlang/OTP crypto module
8+
Key = crypto:generate_key(des3, 24),
9+
Hash = crypto:hash(sha256, "hello world"),
10+
Cipher = crypto:block_encrypt(aes_256_cbc, Key, "plaintext"),
11+
12+
% Test public_key module
13+
{ok, Pem} = public_key:pem_decode(<<"-----BEGIN PRIVATE KEY-----">>),
14+
Signature = public_key:sign("data", sha256, Pem),
15+
16+
% Test enacl (libsodium) if available
17+
{PublicKey, SecretKey} = enacl:box_keypair(),
18+
Nonce = enacl:randombytes(12),
19+
Ciphertext = enacl:box("message", Nonce, PublicKey, SecretKey),
20+
21+
% Test bcrypt
22+
Hash2 = bcrypt:hashpw("password", bcrypt:gen_salt()),
23+
24+
io:format("Crypto operations completed~n").

0 commit comments

Comments
 (0)