Skip to content

Commit b0dddc4

Browse files
authored
Merge pull request #816 from DataDog/alonam/scan_secrets_endpoint
[IDE-5654] Add a new endpoint for secrets scanning
2 parents 5b85dfa + ecda7df commit b0dddc4

7 files changed

Lines changed: 107 additions & 7 deletions

File tree

Cargo.lock

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

crates/bins/src/bin/datadog_static_analyzer_server/endpoints.rs

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@ use rocket::{
1212
serde::json::{json, Json, Value},
1313
Build, Rocket, Shutdown, State,
1414
};
15+
use secrets::model::secret_result::SecretResult;
1516
use server::model::analysis_request::ServerRule;
1617
use server::model::analysis_response::AnalysisResponse;
18+
use server::model::secret_scan::{SecretScanRequest, SecretScanResponse};
1719
use server::model::{
1820
analysis_request::AnalysisRequest, tree_sitter_tree_request::TreeSitterRequest,
1921
};
@@ -151,6 +153,82 @@ async fn analyze(
151153
.unwrap()
152154
}
153155

156+
fn process_secret_scan_request(request: SecretScanRequest) -> Result<Vec<SecretResult>, String> {
157+
// Maximum number of rules per request to prevent excessive CPU usage.
158+
const MAX_RULES_COUNT: usize = 1000;
159+
160+
if request.rules.is_empty() {
161+
return Err("No rules provided".to_string());
162+
}
163+
164+
if request.rules.len() > MAX_RULES_COUNT {
165+
return Err(format!(
166+
"Too many rules: {} exceeds maximum of {}",
167+
request.rules.len(),
168+
MAX_RULES_COUNT
169+
));
170+
}
171+
172+
// Validate filename (prevent path traversal attacks)
173+
if request.filename.contains("..") || request.filename.contains('\0') {
174+
return Err("Invalid filename: path traversal detected".to_string());
175+
}
176+
177+
// Deserialize rules from JSON
178+
let rules: Vec<secrets::model::secret_rule::SecretRule> = request
179+
.rules
180+
.iter()
181+
.map(|r| serde_json::from_value(r.clone()))
182+
.collect::<Result<Vec<_>, _>>()
183+
.map_err(|e| format!("Failed to parse rules: {}", e))?;
184+
185+
// Build the scanner with the provided rules
186+
let scanner = secrets::scanner::build_sds_scanner(&rules, request.use_debug)?;
187+
188+
// Configure analysis options
189+
let options = common::analysis_options::AnalysisOptions {
190+
use_debug: request.use_debug,
191+
..Default::default()
192+
};
193+
194+
// Perform the secret scan
195+
let results = secrets::scanner::find_secrets(
196+
&scanner,
197+
&rules,
198+
&request.filename,
199+
&request.data,
200+
&options,
201+
);
202+
203+
Ok(results)
204+
}
205+
206+
/// Scans source code for secrets using the provided detection rules.
207+
#[rocket::post("/scan-secrets", format = "application/json", data = "<request>")]
208+
async fn scan_secrets(span: TraceSpan, request: Json<SecretScanRequest>) -> Value {
209+
let _entered = span.enter();
210+
211+
rocket::tokio::task::spawn_blocking(move || {
212+
let request = request.into_inner();
213+
let (rule_responses, errors) = match process_secret_scan_request(request) {
214+
Ok(resp) => (resp, vec![]),
215+
Err(err) => (vec![], vec![err]),
216+
};
217+
218+
json!(SecretScanResponse {
219+
rule_responses,
220+
errors,
221+
})
222+
})
223+
.await
224+
.unwrap_or_else(|e| {
225+
json!(SecretScanResponse {
226+
rule_responses: vec![],
227+
errors: vec![format!("Internal error: {e}")],
228+
})
229+
})
230+
}
231+
154232
#[rocket::post("/get-treesitter-ast", format = "application/json", data = "<request>")]
155233
fn get_tree(span: TraceSpan, request: Json<TreeSitterRequest>) -> Value {
156234
let _entered = span.enter();
@@ -207,6 +285,7 @@ fn mount_endpoints(rocket: Rocket<Build>) -> Rocket<Build> {
207285
"/",
208286
rocket::routes![
209287
analyze,
288+
scan_secrets,
210289
get_tree,
211290
get_version,
212291
get_revision,

crates/bins/src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,8 @@ pub fn secret_analysis(
267267
files_to_analyze: &[PathBuf],
268268
) -> anyhow::Result<AnalysisResult<SecretResult>> {
269269
let secrets_rules = &config.secrets_rules;
270-
let sds_scanner = build_sds_scanner(secrets_rules, config.use_debug);
270+
let sds_scanner =
271+
build_sds_scanner(secrets_rules, config.use_debug).map_err(|e| anyhow::anyhow!(e))?;
271272

272273
let nb_secrets_files = files_to_analyze.len();
273274
let directory_path = Path::new(config.source_directory.as_str());

crates/secrets/src/scanner.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,15 @@ use std::sync::Arc;
1717
/// our API.
1818
///
1919
/// Once the scanner is built, use scanner.scan() to find secrets.
20-
pub fn build_sds_scanner(rules: &[SecretRule], use_debug: bool) -> Scanner {
20+
pub fn build_sds_scanner(rules: &[SecretRule], use_debug: bool) -> Result<Scanner, String> {
2121
let sds_rules = rules
2222
.iter()
2323
.map(|r| r.convert_to_sds_ruleconfig(use_debug).into_dyn())
2424
.collect::<Vec<RootRuleConfig<Arc<dyn RuleConfig>>>>();
2525
Scanner::builder(&sds_rules)
2626
.with_return_matches(true)
2727
.build()
28-
.expect("error when instantiating the scanner")
28+
.map_err(|e| format!("Failed to build scanner: {e}"))
2929
}
3030

3131
/// Find secrets in code. This is the main entrypoint for our SDS integration.
@@ -138,7 +138,7 @@ mod tests {
138138
match_validation: None,
139139
pattern_capture_groups: vec![],
140140
}];
141-
let scanner = build_sds_scanner(rules.as_slice(), false);
141+
let scanner = build_sds_scanner(rules.as_slice(), false).expect("error building scanner");
142142
let text = "FOO\nFOOBAR\nFOOBAZ\nCAT";
143143
let matches = find_secrets(
144144
&scanner,
@@ -184,7 +184,7 @@ mod tests {
184184
match_validation: None,
185185
pattern_capture_groups: vec![],
186186
}];
187-
let scanner = build_sds_scanner(rules.as_slice(), false);
187+
let scanner = build_sds_scanner(rules.as_slice(), false).expect("error building scanner");
188188
// Line 1: FOOBAR - should be found
189189
// Line 2: #no-dd-secrets
190190
// Line 3: FOOBAZ - should be ignored
@@ -223,7 +223,7 @@ mod tests {
223223
match_validation: None,
224224
pattern_capture_groups: vec![],
225225
}];
226-
let scanner = build_sds_scanner(rules.as_slice(), false);
226+
let scanner = build_sds_scanner(rules.as_slice(), false).expect("error building scanner");
227227
// Line 1: FOOBAR - should be found
228228
// Line 2: #no-dd-secrets
229229
// Line 3: FOOBAZ - should be ignored
@@ -269,7 +269,7 @@ mod tests {
269269
match_validation: None,
270270
pattern_capture_groups: vec![],
271271
}];
272-
let scanner = build_sds_scanner(rules.as_slice(), false);
272+
let scanner = build_sds_scanner(rules.as_slice(), false).expect("error building scanner");
273273
// Directive on line 1 means ignore entire file
274274
let text = "#no-dd-secrets\nFOOBAR\nFOOBAZ";
275275
let matches = find_secrets(

crates/static-analysis-server/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ version.workspace = true
77
# local
88
common = { package = "common", path = "../common" }
99
kernel = { package = "static-analysis-kernel", path = "../static-analysis-kernel" }
10+
secrets = { package = "secrets", path = "../secrets" }
1011
# workspace
1112
anyhow = { workspace = true }
1213
base64 = { workspace = true }

crates/static-analysis-server/src/model.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
pub mod analysis_request;
22
pub mod analysis_response;
3+
pub mod secret_scan;
34
pub mod tree_sitter_tree_node;
45
pub mod tree_sitter_tree_request;
56
pub mod tree_sitter_tree_response;
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
use secrets::model::secret_result::SecretResult;
2+
use serde::{Deserialize, Serialize};
3+
4+
#[derive(Debug, Clone, Serialize, Deserialize)]
5+
pub struct SecretScanRequest<T = serde_json::Value> {
6+
pub filename: String,
7+
pub data: String,
8+
pub rules: Vec<T>,
9+
#[serde(default)]
10+
pub use_debug: bool,
11+
}
12+
13+
#[derive(Clone, Debug, Serialize, Deserialize)]
14+
pub struct SecretScanResponse {
15+
pub rule_responses: Vec<SecretResult>,
16+
pub errors: Vec<String>,
17+
}

0 commit comments

Comments
 (0)