Skip to content

Commit 9658830

Browse files
authored
Add dynamic library loading support and external detector example (#17)
1 parent 9068ae2 commit 9658830

File tree

8 files changed

+236
-15
lines changed

8 files changed

+236
-15
lines changed

.vscode/launch.json

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,12 @@
2323
// "metadata"
2424
"scan",
2525
"${workspaceFolder}/corpus/subdir",
26-
"--project-root",
27-
"${workspaceFolder}/corpus",
28-
"--detectors",
29-
"all"
26+
// "--project-root",
27+
// "${workspaceFolder}/corpus",
28+
// "--detectors",
29+
// "all"
30+
"--load",
31+
"${workspaceFolder}/target/debug/libexternal_detector_example.so"
3032
],
3133
"cwd": "${workspaceFolder}"
3234
},

compact-scanner/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ repository = { workspace = true }
1010
clap = { version = "4.5.32", features = ["derive"] }
1111
serde_json = { version = "1.0", features = ["preserve_order"] }
1212
serde_yaml = "0.9.17"
13+
libloading = "0.8.6"
1314
compact-security-detectors.workspace = true
1415
compact-security-detectors-sdk.workspace = true
1516

compact-scanner/README.md

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ CLI tool for scanning `.compact` files using Compact security detectors.
44

55
## Overview
66

7-
The **compact-scanner** is a command-line tool designed to scan `.compact` files for security vulnerabilities using the Compact security detectors. It provides a simple interface to run various detectors and output results in JSON format.
7+
The **compact-scanner** is a command-line tool designed to scan `.compact` files for security vulnerabilities using the
8+
Compact security detectors. It provides a simple interface to run various detectors and output results in JSON format.
89

910
## CLI Usage
1011

@@ -13,11 +14,13 @@ The **compact-scanner** is a command-line tool designed to scan `.compact` files
1314
### Modes
1415

1516
- `metadata` : Print scanner metadata as JSON and exit.
16-
- `scan <PATH>` : Scan files in the specified directory or file. The scanner will recursively scan all files in the directory and its subdirectories.
17+
- `scan <PATH>` : Scan files in the specified directory or file. The scanner will recursively scan all files in the
18+
directory and its subdirectories.
1719

1820
### Options
1921

20-
- `--detectors <NAME>...` : Optional list of detector names to run. If omitted, all available detectors will run. You can use `all` to run all detectors.
22+
- `--detectors <NAME>...` : Optional list of detector names to run. If omitted, all available detectors will run. You
23+
can use `all` to run all detectors.
2124
- `--project-root <PATH>` : Optional project root path to calculate relative file paths in output.
2225

2326
### Examples
@@ -41,6 +44,7 @@ compact-scanner scan src --project-root .
4144
> How **compact-scanner** discovers and executes security detectors.
4245
4346
Located in `src/main.rs`, the function:
47+
4448
```rust
4549
fn available_detectors() -> Vec<CompactDetector> {
4650
all_detectors()
@@ -51,14 +55,17 @@ fn available_detectors() -> Vec<CompactDetector> {
5155
```
5256

5357
### Built-in vs. Custom Detectors
58+
5459
- **Built-in**: Provided by `compact-security-detectors::all_detectors()`.
5560
- **Custom**: Placeholder via `custom_detectors()` (currently returns empty).
5661

5762
### Selecting Detectors
63+
5864
- Omit `--detectors`: run all discovered detectors.
5965
- Provide `--detectors NAME...`: filter by detector `name()` property.
6066

6167
### Execution Flow
68+
6269
1. Build the in-memory codebase: `build_codebase(files)` from the SDK.
6370
2. Iterate over selected detectors and run `detector.check(&codebase)`.
6471
3. Collect `DetectorResult` for detectors that return findings.
@@ -70,6 +77,7 @@ fn available_detectors() -> Vec<CompactDetector> {
7077
### Metadata Output
7178

7279
When invoked with `metadata`, the JSON includes:
80+
7381
```json
7482
{
7583
"name": "compact_scanner",
@@ -82,8 +90,13 @@ When invoked with `metadata`, the JSON includes:
8290
"description": "Detector Description",
8391
"report": {
8492
"severity": "<severity>",
85-
"tags": ["tag1", "tag2"],
86-
"template": { /* YAML converted to JSON */ }
93+
"tags": [
94+
"tag1",
95+
"tag2"
96+
],
97+
"template": {
98+
/* YAML converted to JSON */
99+
}
87100
}
88101
}
89102
]
@@ -93,10 +106,14 @@ When invoked with `metadata`, the JSON includes:
93106
### Scan Results Output
94107

95108
When scanning code files, the JSON structure is:
109+
96110
```json
97111
{
98112
"errors": [],
99-
"files_scanned": ["relative/path1.compact", "path2.compact"],
113+
"files_scanned": [
114+
"relative/path1.compact",
115+
"path2.compact"
116+
],
100117
"detector_responses": {
101118
"DetectorName": {
102119
"finding": {
@@ -132,3 +149,26 @@ See [style guidelines](../style_guidelines.md) for coding standards and best pra
132149
## License
133150

134151
[AGPLv3](../LICENSE)
152+
153+
## Experimental and Risk-Prone Feature
154+
155+
> This functionality is intended for controlled testing environments only. It has not undergone extensive validation,
156+
> and its behavior may be unpredictable. Please ensure that you fully understand the potential risks and implications
157+
> before using it in any production scenario.
158+
159+
160+
For those who do not want to re-compile the `scanner` or `detectors` crates, dynamic library loading is available
161+
with the `--load` flag.
162+
163+
Usage:
164+
165+
```bash
166+
compact-scanner scan <PATH> --load <PATH_TO_DETECTOR_LIBRARY>
167+
```
168+
169+
This will load the dynamic library at runtime. The library must contain the `CompactDetector` trait implementation and
170+
be compiled with the same Rust and `sdk` versions as the scanner.
171+
External detector must export the "external_detector" symbol.
172+
External detector must implement `DetectorReportTemplate` trait.
173+
174+
See the example [external detector](../examples/external-detector/src/lib.rs) for more details.

compact-scanner/src/main.rs

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
use std::collections::HashMap;
2-
31
use clap::Parser;
42
use compact_security_detectors::all_detectors;
53
use compact_security_detectors_sdk::{
64
build_codebase,
75
detector::{CompactDetector, DetectorResult},
86
};
7+
use libloading::{Library, Symbol};
98
use parser::Cli;
109
use serde_json::{json, Map};
10+
use std::collections::HashMap;
1111

1212
mod parser;
1313

@@ -19,6 +19,7 @@ fn main() {
1919
code,
2020
detectors,
2121
project_root,
22+
load_lib,
2223
} => {
2324
let mut corpus = HashMap::new();
2425
for path in &code {
@@ -46,7 +47,7 @@ fn main() {
4647
}
4748
}
4849
if !corpus.is_empty() {
49-
let result = execute_rules(&corpus, detectors);
50+
let result = execute_detectors(&corpus, detectors, load_lib);
5051

5152
let files_scanned: Vec<String> = corpus
5253
.keys()
@@ -84,11 +85,25 @@ fn main() {
8485
}
8586
}
8687

87-
fn execute_rules(
88+
fn execute_detectors(
8889
files: &HashMap<String, String>,
8990
rules: Option<Vec<String>>,
91+
load_lib: Option<std::path::PathBuf>,
9092
) -> HashMap<String, Vec<DetectorResult>> {
9193
let codebase = build_codebase(files).unwrap();
94+
let mut results = HashMap::new();
95+
if let Some(load_lib) = load_lib {
96+
unsafe {
97+
let lib = Library::new(load_lib).unwrap();
98+
let constructor: Symbol<unsafe extern "C" fn() -> CompactDetector> =
99+
lib.get(b"external_detector").unwrap();
100+
let detector = constructor();
101+
let detector_result = detector.check(codebase.as_ref());
102+
if let Some(errors) = detector_result {
103+
results.insert(detector.id().to_string(), errors);
104+
}
105+
}
106+
}
92107
let selected_detectors: Vec<_> = available_detectors()
93108
.into_iter()
94109
.filter(|detector| {
@@ -101,7 +116,6 @@ fn execute_rules(
101116
})
102117
.collect();
103118

104-
let mut results = HashMap::new();
105119
for detector in selected_detectors {
106120
let detector_result = detector.check(codebase.as_ref());
107121
if let Some(errors) = detector_result {

compact-scanner/src/parser.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ pub(crate) enum Commands {
88
detectors: Option<Vec<String>>,
99
#[arg(long = "project-root", required = false, value_parser)]
1010
project_root: Option<std::path::PathBuf>,
11+
#[arg(long = "load", required = false, value_parser)]
12+
load_lib: Option<std::path::PathBuf>,
1113
},
1214
Metadata,
1315
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[package]
2+
name = "external-detector-example"
3+
version = "0.0.1"
4+
edition = "2021"
5+
6+
[lib]
7+
crate-type = ["cdylib"]
8+
9+
[dependencies]
10+
compact-security-detectors-sdk = { path = "../../sdk" }
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
use compact_security_detectors_sdk::{
2+
ast::{
3+
declaration::Declaration, definition::Definition, expression::Expression,
4+
node_type::NodeType, ty::Type,
5+
},
6+
codebase::{Codebase, SealedState},
7+
detector::{CompactDetector, DetectorOpaque, DetectorReportTemplate, DetectorResult},
8+
};
9+
use std::collections::HashMap;
10+
11+
compact_security_detectors_sdk::detector! {
12+
13+
#[type_name = ArrayLoopBoundCheck]
14+
fn array_loop_bound_check(
15+
codebase: &Codebase<SealedState>,
16+
) -> Option<Vec<DetectorResult>> {
17+
let mut errors = Vec::new();
18+
for for_stmt in codebase.list_for_statement_nodes() {
19+
let index_access_expressions = codebase.get_children_cmp(for_stmt.id, |n| {
20+
matches!(n, NodeType::Expression(Expression::IndexAccess(_)))
21+
});
22+
let upper_bound = for_stmt.upper_bound_nat();
23+
if upper_bound.is_none() {
24+
continue;
25+
}
26+
let upper_bound = upper_bound.unwrap();
27+
28+
for index_access in index_access_expressions {
29+
if let NodeType::Expression(Expression::IndexAccess(index_access)) = index_access {
30+
let arr_type = codebase.get_symbol_type_by_id(index_access.base.id());
31+
if let Some(Type::Vector(t_vec)) = arr_type {
32+
if t_vec.size_nat().unwrap_or(0) >= upper_bound {
33+
let parent = codebase.get_parent_container(index_access.id);
34+
let mut parent_type = "circuit";
35+
let parent_name = match parent {
36+
Some(NodeType::Definition(Definition::Circuit(c))) => c.name(),
37+
Some(NodeType::Declaration(Declaration::Constructor(_))) => {
38+
parent_type = "constructor";
39+
String::default()
40+
}
41+
_ => String::from("Unknown"),
42+
};
43+
errors.push(
44+
DetectorResult {
45+
file_path: codebase.find_node_file(index_access.id).unwrap().fname,
46+
offset_start: index_access.location.offset_start,
47+
offset_end: index_access.location.offset_end,
48+
extra: {
49+
let mut map = HashMap::new();
50+
map.insert("ARRAY_INDEX_ACCESS".to_string(), index_access.location.source.clone());
51+
map.insert("PARENT_NAME".to_string(), parent_name);
52+
map.insert("PARENT_TYPE".to_string(), parent_type.to_string());
53+
Some(map)
54+
},
55+
},
56+
);
57+
}
58+
}
59+
}
60+
}
61+
}
62+
if errors.is_empty() {
63+
None
64+
} else {
65+
Some(errors)
66+
}
67+
}
68+
}
69+
70+
impl DetectorReportTemplate for ArrayLoopBoundCheck {
71+
fn id(&self) -> String {
72+
String::from("array-loop-bound-check")
73+
}
74+
fn uid(&self) -> String {
75+
String::from("3fTuAe")
76+
}
77+
fn description(&self) -> String {
78+
String::from("Detects potential out-of-bounds array index accesses within loops, which can cause runtime errors or unexpected behavior.")
79+
}
80+
fn severity(&self) -> String {
81+
String::from("medium")
82+
}
83+
fn tags(&self) -> Vec<String> {
84+
vec![
85+
String::from("audit"),
86+
String::from("reportable"),
87+
String::from("compact"),
88+
]
89+
}
90+
fn title_single_instance(&self) -> String {
91+
String::from("Potential Out-of-Bounds Array Index Access Detected")
92+
}
93+
fn title_multiple_instance(&self) -> String {
94+
String::from(
95+
"The following potential out-of-bounds array index accesses were found in the code:",
96+
)
97+
}
98+
fn opening(&self) -> String {
99+
String::from("Accessing array elements outside their valid index range can lead to runtime errors, security vulnerabilities, or unpredictable program behavior. This issue typically occurs when a loop iterates beyond the array's defined bounds, resulting in unsafe memory access.")
100+
}
101+
fn body_single_file_single_instance(&self) -> String {
102+
String::from("In `$file_name`, a potential out-of-bounds array index access was detected in the `$PARENT_NAME` $PARENT_TYPE on line $instance_line. The array access statement `$ARRAY_INDEX_ACCESS` may exceed the array's valid index range.")
103+
}
104+
fn body_single_file_multiple_instance(&self) -> String {
105+
String::from("In `$file_name`, multiple potential out-of-bounds array index accesses were detected. Review each instance below to ensure array accesses remain within valid bounds.")
106+
}
107+
fn body_multiple_file_multiple_instance(&self) -> String {
108+
String::from("Across $total_files files, multiple potential out-of-bounds array index accesses were detected. Review each instance below to ensure array accesses remain within valid bounds.")
109+
}
110+
fn body_list_item_single_file(&self) -> String {
111+
{
112+
"{body_list_item}".to_string()
113+
}
114+
}
115+
fn body_list_item_multiple_file(&self) -> String {
116+
{
117+
"{body_list_item}".to_string()
118+
}
119+
}
120+
fn closing(&self) -> String {
121+
String::from("To resolve this issue, ensure that all array index accesses within loops are properly bounded and do not exceed the array's size. Failing to address this may result in runtime exceptions, data corruption, or exploitable vulnerabilities.")
122+
}
123+
fn template(&self) -> String {
124+
String::from(
125+
r#"template:
126+
title: Potential Out-of-Bounds Array Index Access Detected
127+
opening: Accessing array elements outside their valid index range can lead to runtime errors, security vulnerabilities, or unpredictable program behavior. This issue typically occurs when a loop iterates beyond the array's defined bounds, resulting in unsafe memory access.
128+
body-single-file-single-instance: |
129+
In `$file_name`, a potential out-of-bounds array index access was detected in the `$PARENT_NAME` $PARENT_TYPE on line $instance_line. The array access statement `$ARRAY_INDEX_ACCESS` may exceed the array's valid index range.
130+
body-single-file-multiple-instance: |
131+
In `$file_name`, multiple potential out-of-bounds array index accesses were detected. Review each instance below to ensure array accesses remain within valid bounds.
132+
body-multiple-file-multiple-instance: |
133+
Across $total_files files, multiple potential out-of-bounds array index accesses were detected. Review each instance below to ensure array accesses remain within valid bounds.
134+
body-list-item-intro: 'The following potential out-of-bounds array index accesses were found in the code:'
135+
body-list-item-always: '- The `$ARRAY_INDEX_ACCESS` statement in the `$PARENT_NAME` $PARENT_TYPE on line $instance_line of [`$file_name`]($instance_line_link)'
136+
closing: |
137+
To resolve this issue, ensure that all array index accesses within loops are properly bounded and do not exceed the array's size. Failing to address this may result in runtime exceptions, data corruption, or exploitable vulnerabilities.
138+
"#,
139+
)
140+
}
141+
}
142+
143+
#[no_mangle]
144+
pub extern "C" fn external_detector() -> *mut DetectorOpaque {
145+
let detector: CompactDetector = Box::new(ArrayLoopBoundCheck);
146+
Box::into_raw(detector) as *mut DetectorOpaque
147+
}

sdk/src/detector.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ macro_rules! detectors {
5454
() => {};
5555
}
5656

57+
#[repr(C)]
58+
pub struct DetectorOpaque {
59+
_private: [u8; 0],
60+
}
61+
5762
pub trait CombinedDetector: Detector + DetectorReportTemplate {}
5863

5964
impl<T: Detector + DetectorReportTemplate> CombinedDetector for T {}

0 commit comments

Comments
 (0)