use crate::detectors::base::{Detector, DetectorConfig};
use crate::graph::GraphQueryExt;
use crate::models::{deterministic_finding_id, Finding, Severity};
use anyhow::Result;
use regex::Regex;
use std::collections::HashSet;
use std::path::PathBuf;
use std::sync::LazyLock;
use tracing::info;
static INSECURE_RANDOM: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"(?i)(Math\.random\(\)|random\.random\(\)|random\.randint|rand\(\)|srand\(|mt_rand|lcg_value|uniqid)").expect("valid regex")
});
fn get_secure_alternative(ext: &str) -> &'static str {
match ext {
"py" => {
"```python\n\
import secrets\n\
\n\
# For tokens/passwords\n\
token = secrets.token_urlsafe(32)\n\
\n\
# For random integers\n\
num = secrets.randbelow(100)\n\
\n\
# For random bytes\n\
data = secrets.token_bytes(16)\n\
```"
}
"js" | "ts" => {
"```javascript\n\
// Node.js\n\
const crypto = require('crypto');\n\
const token = crypto.randomBytes(32).toString('hex');\n\
\n\
// Browser\n\
const array = new Uint8Array(32);\n\
crypto.getRandomValues(array);\n\
```"
}
"java" => {
"```java\n\
import java.security.SecureRandom;\n\
\n\
SecureRandom random = new SecureRandom();\n\
byte[] bytes = new byte[32];\n\
random.nextBytes(bytes);\n\
```"
}
"go" => {
"```go\n\
import \"crypto/rand\"\n\
\n\
bytes := make([]byte, 32)\n\
rand.Read(bytes)\n\
```"
}
"php" => {
"```php\n\
// PHP 7+\n\
$bytes = random_bytes(32);\n\
$token = bin2hex($bytes);\n\
```"
}
"rb" => {
"```ruby\n\
require 'securerandom'\n\
\n\
token = SecureRandom.hex(32)\n\
```"
}
"c" | "cpp" => {
"```c\n\
// Linux\n\
#include <sys/random.h>\n\
getrandom(buffer, size, 0);\n\
\n\
// Or read from /dev/urandom\n\
```"
}
_ => "Use your platform's cryptographic random number generator.",
}
}
pub struct InsecureRandomDetector {
#[allow(dead_code)] repository_path: PathBuf,
max_findings: usize,
}
impl InsecureRandomDetector {
crate::detectors::detector_new!(50);
fn analyze_usage(line: &str, surrounding: &str) -> (SecurityContext, String) {
let combined = format!("{} {}", line, surrounding).to_lowercase();
if combined.contains("token") || combined.contains("secret") || combined.contains("api_key")
{
return (
SecurityContext::Token,
"token/secret generation".to_string(),
);
}
if combined.contains("password") || combined.contains("salt") || combined.contains("hash") {
return (
SecurityContext::Password,
"password/salt generation".to_string(),
);
}
if combined.contains("session") || combined.contains("auth") || combined.contains("login") {
return (
SecurityContext::Session,
"session/authentication".to_string(),
);
}
if combined.contains("uuid") || combined.contains("identifier") {
return (SecurityContext::ID, "ID generation".to_string());
}
if (combined.contains("session_id")
|| combined.contains("sessionid")
|| combined.contains("user_id")
|| combined.contains("userid")
|| combined.contains("auth_id")
|| combined.contains("api_id"))
&& !combined.contains("trace")
&& !combined.contains("metric")
&& !combined.contains("display")
&& !combined.contains("record")
&& !combined.contains("internal")
&& !combined.contains("log")
{
return (SecurityContext::ID, "ID generation".to_string());
}
if combined.contains("crypto")
|| combined.contains("encrypt")
|| combined.contains("key")
|| combined.contains("iv")
|| combined.contains("nonce")
{
return (
SecurityContext::Crypto,
"cryptographic operation".to_string(),
);
}
if combined.contains("otp")
|| combined.contains("code")
|| combined.contains("verification")
|| combined.contains("pin")
{
return (SecurityContext::OTP, "OTP/verification code".to_string());
}
(SecurityContext::Unknown, "unknown".to_string())
}
fn find_security_callers(
graph: &dyn crate::graph::GraphQuery,
func_name: &str,
func_map: &std::collections::HashMap<String, crate::graph::store_models::CodeNode>,
) -> Vec<String> {
let i = graph.interner();
let mut security_callers = Vec::new();
if let Some(func) = func_map.get(func_name) {
let callers = graph.get_callers(func.qn(i));
for caller in callers {
let caller_lower = caller.node_name(i).to_lowercase();
if caller_lower.contains("auth")
|| caller_lower.contains("login")
|| caller_lower.contains("token")
|| caller_lower.contains("session")
|| caller_lower.contains("password")
|| caller_lower.contains("secret")
{
security_callers.push(caller.node_name(i).to_string());
}
}
}
security_callers
}
}
#[derive(PartialEq)]
enum SecurityContext {
Token,
Password,
Session,
ID,
Crypto,
OTP,
Unknown,
}
impl Detector for InsecureRandomDetector {
fn name(&self) -> &'static str {
"insecure-random"
}
fn description(&self) -> &'static str {
"Detects insecure random for security purposes"
}
fn bypass_postprocessor(&self) -> bool {
true
}
fn file_extensions(&self) -> &'static [&'static str] {
&["py", "js", "ts", "jsx", "tsx", "rb", "java", "go"]
}
fn detect(
&self,
ctx: &crate::detectors::analysis_context::AnalysisContext,
) -> Result<Vec<Finding>> {
let graph = ctx.graph;
let files = &ctx.as_file_provider();
let _i = graph.interner();
let mut findings = vec![];
let mut func_map: Option<
std::collections::HashMap<String, crate::graph::store_models::CodeNode>,
> = None;
for path in
files.files_with_extensions(&["py", "js", "ts", "java", "go", "rb", "php", "c", "cpp"])
{
if findings.len() >= self.max_findings {
break;
}
let path_str = path.to_string_lossy().to_string();
if crate::detectors::base::is_test_path(&path_str) {
continue;
}
let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
let raw = match files.content(path) {
Some(c) => c,
None => continue,
};
if !raw.contains("random")
&& !raw.contains("rand(")
&& !raw.contains("srand(")
&& !raw.contains("mt_rand")
&& !raw.contains("lcg_value")
&& !raw.contains("uniqid")
{
continue;
}
if let Some(content) = files.masked_content(path) {
let lines: Vec<&str> = content.lines().collect();
for (i, line) in lines.iter().enumerate() {
let prev_line = if i > 0 { Some(lines[i - 1]) } else { None };
if crate::detectors::is_line_suppressed(line, prev_line) {
continue;
}
if INSECURE_RANDOM.is_match(line) {
let start = i.saturating_sub(5);
let end = (i + 5).min(lines.len());
let surrounding = lines[start..end].join(" ");
let (context, usage) = Self::analyze_usage(line, &surrounding);
let containing_func =
graph.find_function_at(&path_str, (i + 1) as u32).map(|f| {
f.node_name(crate::graph::interner::global_interner())
.to_string()
});
let security_callers = if let Some(ref func) = containing_func {
let map = func_map.get_or_insert_with(|| {
graph
.get_functions()
.into_iter()
.map(|f| {
(
f.node_name(crate::graph::interner::global_interner())
.to_string(),
f,
)
})
.collect()
});
Self::find_security_callers(graph, func, map)
} else {
vec![]
};
if context == SecurityContext::Unknown && security_callers.is_empty() {
continue;
}
if context == SecurityContext::ID && security_callers.is_empty() {
let line_lower = line.to_lowercase();
let is_safe_id = line_lower.contains("traceid")
|| line_lower.contains("trace_id")
|| line_lower.contains("metricid")
|| line_lower.contains("metric_id")
|| line_lower.contains("displayid")
|| line_lower.contains("display_id")
|| line_lower.contains("recordid")
|| line_lower.contains("record_id")
|| line_lower.contains("gameid")
|| line_lower.contains("game_id")
|| line_lower.contains("itemid")
|| line_lower.contains("item_id")
;
let is_game_or_ui = line_lower.contains("game")
|| line_lower.contains("jitter")
|| line_lower.contains("color")
|| line_lower.contains("animation")
|| line_lower.contains("position")
|| line_lower.contains("offset")
|| line_lower.contains("delay");
if is_safe_id || is_game_or_ui {
continue;
}
}
let severity = match context {
SecurityContext::Crypto | SecurityContext::Password => {
Severity::Critical
}
SecurityContext::Token
| SecurityContext::Session
| SecurityContext::OTP => Severity::High,
SecurityContext::ID => Severity::Medium,
SecurityContext::Unknown if !security_callers.is_empty() => {
Severity::High
}
_ => Severity::Medium,
};
let mut notes = Vec::new();
notes.push(format!("🎯 Used for: {}", usage));
if let Some(func) = &containing_func {
notes.push(format!("📦 In function: `{}`", func));
}
if !security_callers.is_empty() {
notes.push(format!(
"⚠️ Called by security functions: {}",
security_callers.join(", ")
));
}
let context_notes = format!("\n\n**Analysis:**\n{}", notes.join("\n"));
let random_func = INSECURE_RANDOM
.find(line)
.map(|m| m.as_str())
.unwrap_or("random");
findings.push(Finding {
id: String::new(),
detector: "InsecureRandomDetector".to_string(),
severity,
title: format!("Insecure `{}` used for {}", random_func, usage),
description: format!(
"`{}` is not cryptographically secure and can be predicted by attackers.{}",
random_func, context_notes
),
affected_files: vec![path.to_path_buf()],
line_start: Some((i + 1) as u32),
line_end: Some((i + 1) as u32),
suggested_fix: Some(format!(
"Use a cryptographically secure random number generator:\n\n{}",
get_secure_alternative(ext)
)),
estimated_effort: Some("15 minutes".to_string()),
category: Some("security".to_string()),
cwe_id: Some("CWE-330".to_string()),
why_it_matters: Some(
"Insecure random number generators (like Math.random or random.random) \
use predictable algorithms. Attackers can often guess the output and \
forge tokens, guess passwords, or bypass authentication.".to_string()
),
..Default::default()
});
}
}
}
}
info!(
"InsecureRandomDetector found {} findings (graph-aware)",
findings.len()
);
Ok(findings)
}
}
impl crate::detectors::RegisteredDetector for InsecureRandomDetector {
fn create(init: &crate::detectors::DetectorInit) -> std::sync::Arc<dyn Detector> {
std::sync::Arc::new(Self::new(init.repo_path))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::graph::builder::GraphBuilder;
#[test]
fn test_detects_insecure_random_in_security_context() {
let store = GraphBuilder::new().freeze();
let detector = InsecureRandomDetector::new("/mock/repo");
let ctx = crate::detectors::analysis_context::AnalysisContext::test_with_mock_files(&store, vec![
("auth.py", "import random\n\ndef generate_token():\n token = random.random()\n return str(token)\n"),
]);
let findings = detector.detect(&ctx).expect("detection should succeed");
assert!(
!findings.is_empty(),
"Should detect random.random() used for token generation"
);
assert!(
findings.iter().any(|f| f.title.contains("random.random()")),
"Finding should mention random.random(). Titles: {:?}",
findings.iter().map(|f| &f.title).collect::<Vec<_>>()
);
}
#[test]
fn test_no_finding_for_non_security_random() {
let store = GraphBuilder::new().freeze();
let detector = InsecureRandomDetector::new("/mock/repo");
let ctx = crate::detectors::analysis_context::AnalysisContext::test_with_mock_files(
&store,
vec![(
"simulation.py",
"import random\n\ndef roll_dice():\n return random.randint(1, 6)\n",
)],
);
let findings = detector.detect(&ctx).expect("detection should succeed");
assert!(
findings.is_empty(),
"Should not flag random used in non-security context, but got: {:?}",
findings.iter().map(|f| &f.title).collect::<Vec<_>>()
);
}
}