use crate::flow::cfg::{BasicBlock, BlockId, CFG, Terminator};
use crate::flow::dataflow::{DataflowResult, Direction, TransferFunction, find_node_by_id};
use crate::semantics::LanguageSemantics;
use std::collections::{HashMap, HashSet, VecDeque};
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
pub enum SecurityLabel {
#[default]
Public = 0,
Confidential = 1,
Secret = 2,
TopSecret = 3,
}
impl SecurityLabel {
#[inline]
pub fn can_flow_to(self, target: SecurityLabel) -> bool {
self <= target
}
#[inline]
pub fn join(self, other: SecurityLabel) -> SecurityLabel {
if self >= other { self } else { other }
}
#[inline]
pub fn meet(self, other: SecurityLabel) -> SecurityLabel {
if self <= other { self } else { other }
}
pub fn from_annotation(s: &str) -> Option<SecurityLabel> {
let s_lower = s.to_lowercase();
match s_lower.as_str() {
"public" | "low" | "untrusted" => Some(SecurityLabel::Public),
"confidential" | "internal" | "private" => Some(SecurityLabel::Confidential),
"secret" | "sensitive" | "high" => Some(SecurityLabel::Secret),
"topsecret" | "top_secret" | "top-secret" | "critical" => {
Some(SecurityLabel::TopSecret)
}
_ => None,
}
}
#[inline]
pub fn is_high(self) -> bool {
matches!(self, SecurityLabel::Secret | SecurityLabel::TopSecret)
}
#[inline]
pub fn is_low(self) -> bool {
matches!(self, SecurityLabel::Public)
}
}
impl fmt::Display for SecurityLabel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SecurityLabel::Public => write!(f, "Public"),
SecurityLabel::Confidential => write!(f, "Confidential"),
SecurityLabel::Secret => write!(f, "Secret"),
SecurityLabel::TopSecret => write!(f, "TopSecret"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ImplicitFlow {
pub target_variable: String,
pub source_variables: Vec<String>,
pub assignment_block: BlockId,
pub condition_block: BlockId,
pub flow_type: ImplicitFlowType,
pub source_label: SecurityLabel,
pub target_label: SecurityLabel,
pub assignment_line: Option<usize>,
pub condition_line: Option<usize>,
}
impl ImplicitFlow {
pub fn is_violation(&self) -> bool {
!self.source_label.can_flow_to(self.target_label)
}
pub fn description(&self) -> String {
let sources = self.source_variables.join(", ");
format!(
"{} -> {} via {} ({}->{})",
sources, self.target_variable, self.flow_type, self.source_label, self.target_label
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ImplicitFlowType {
IfCondition,
LoopCondition,
SwitchCase,
TernaryExpression,
ExceptionHandler,
}
impl fmt::Display for ImplicitFlowType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ImplicitFlowType::IfCondition => write!(f, "if-condition"),
ImplicitFlowType::LoopCondition => write!(f, "loop-condition"),
ImplicitFlowType::SwitchCase => write!(f, "switch-case"),
ImplicitFlowType::TernaryExpression => write!(f, "ternary"),
ImplicitFlowType::ExceptionHandler => write!(f, "exception"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ControlDependence {
pub dependent_block: BlockId,
pub controller_block: BlockId,
pub condition_node: Option<usize>,
pub control_type: ImplicitFlowType,
}
#[derive(Debug)]
pub struct ControlDependenceGraph {
pub dependencies: HashMap<BlockId, Vec<ControlDependence>>,
pub dependents: HashMap<BlockId, Vec<BlockId>>,
post_dominators: HashMap<BlockId, BlockId>,
}
impl ControlDependenceGraph {
pub fn build(cfg: &CFG) -> Self {
let mut cdg = Self {
dependencies: HashMap::new(),
dependents: HashMap::new(),
post_dominators: HashMap::new(),
};
if cfg.blocks.is_empty() {
return cdg;
}
cdg.compute_post_dominators(cfg);
cdg.build_dependencies_from_branches(cfg);
cdg
}
fn compute_post_dominators(&mut self, cfg: &CFG) {
let all_blocks: HashSet<BlockId> = cfg.blocks.iter().map(|b| b.id).collect();
let mut post_dom: HashMap<BlockId, HashSet<BlockId>> = HashMap::new();
for block in &cfg.blocks {
if matches!(
block.terminator,
Terminator::Return | Terminator::Unreachable
) {
let mut set = HashSet::new();
set.insert(block.id);
post_dom.insert(block.id, set);
} else {
post_dom.insert(block.id, all_blocks.clone());
}
}
let mut changed = true;
let mut iterations = 0;
let max_iterations = cfg.blocks.len() * 10;
while changed && iterations < max_iterations {
changed = false;
iterations += 1;
for block_id in (0..cfg.blocks.len()).rev() {
let successors = cfg.successors(block_id);
if successors.is_empty() {
continue;
}
let mut new_post_dom: HashSet<BlockId> = all_blocks.clone();
for succ in &successors {
if let Some(succ_dom) = post_dom.get(succ) {
new_post_dom = new_post_dom.intersection(succ_dom).cloned().collect();
}
}
new_post_dom.insert(block_id);
if post_dom.get(&block_id) != Some(&new_post_dom) {
post_dom.insert(block_id, new_post_dom);
changed = true;
}
}
}
for (block_id, dominators) in &post_dom {
let mut candidates: Vec<_> = dominators
.iter()
.filter(|&&d| d != *block_id)
.cloned()
.collect();
candidates.sort();
if let Some(idom) = candidates.first() {
self.post_dominators.insert(*block_id, *idom);
}
}
}
fn build_dependencies_from_branches(&mut self, cfg: &CFG) {
for block in &cfg.blocks {
match &block.terminator {
Terminator::Branch {
condition_node,
true_block,
false_block,
} => {
self.add_branch_dependency(
cfg,
block.id,
*true_block,
Some(*condition_node),
ImplicitFlowType::IfCondition,
);
self.add_branch_dependency(
cfg,
block.id,
*false_block,
Some(*condition_node),
ImplicitFlowType::IfCondition,
);
self.add_transitive_dependencies(cfg, block.id, *true_block, *condition_node);
self.add_transitive_dependencies(cfg, block.id, *false_block, *condition_node);
}
Terminator::Loop {
body,
exit,
condition_node,
} => {
self.add_branch_dependency(
cfg,
block.id,
*body,
*condition_node,
ImplicitFlowType::LoopCondition,
);
if let Some(cond) = condition_node {
self.add_loop_body_dependencies(cfg, block.id, *body, *exit, *cond);
}
}
Terminator::Switch {
condition_node,
cases,
} => {
for (case_node, target) in cases {
let cond = case_node.unwrap_or(*condition_node);
self.add_branch_dependency(
cfg,
block.id,
*target,
Some(cond),
ImplicitFlowType::SwitchCase,
);
}
}
Terminator::TryCatch {
try_block,
catch_block,
..
} => {
if let Some(catch) = catch_block {
self.add_branch_dependency(
cfg,
*try_block,
*catch,
None,
ImplicitFlowType::ExceptionHandler,
);
}
}
_ => {}
}
}
}
fn add_branch_dependency(
&mut self,
_cfg: &CFG,
controller: BlockId,
dependent: BlockId,
condition_node: Option<usize>,
control_type: ImplicitFlowType,
) {
let dep = ControlDependence {
dependent_block: dependent,
controller_block: controller,
condition_node,
control_type,
};
self.dependencies
.entry(dependent)
.or_default()
.push(dep.clone());
self.dependents
.entry(controller)
.or_default()
.push(dependent);
}
fn add_transitive_dependencies(
&mut self,
cfg: &CFG,
controller: BlockId,
start: BlockId,
condition_node: usize,
) {
let mut visited = HashSet::new();
let mut queue = VecDeque::new();
queue.push_back(start);
while let Some(block_id) = queue.pop_front() {
if !visited.insert(block_id) {
continue;
}
if let Some(&ipdom) = self.post_dominators.get(&controller)
&& block_id == ipdom
{
continue;
}
let dep = ControlDependence {
dependent_block: block_id,
controller_block: controller,
condition_node: Some(condition_node),
control_type: ImplicitFlowType::IfCondition,
};
let deps = self.dependencies.entry(block_id).or_default();
if !deps.contains(&dep) {
deps.push(dep);
self.dependents
.entry(controller)
.or_default()
.push(block_id);
}
for succ in cfg.successors(block_id) {
if !visited.contains(&succ) {
queue.push_back(succ);
}
}
}
}
fn add_loop_body_dependencies(
&mut self,
cfg: &CFG,
controller: BlockId,
body_start: BlockId,
exit: BlockId,
condition_node: usize,
) {
let mut visited = HashSet::new();
let mut queue = VecDeque::new();
queue.push_back(body_start);
while let Some(block_id) = queue.pop_front() {
if !visited.insert(block_id) {
continue;
}
if block_id == exit {
continue;
}
let dep = ControlDependence {
dependent_block: block_id,
controller_block: controller,
condition_node: Some(condition_node),
control_type: ImplicitFlowType::LoopCondition,
};
let deps = self.dependencies.entry(block_id).or_default();
if !deps.contains(&dep) {
deps.push(dep);
}
for succ in cfg.successors(block_id) {
if succ != exit && !visited.contains(&succ) {
queue.push_back(succ);
}
}
}
}
pub fn get_dependencies(&self, block_id: BlockId) -> &[ControlDependence] {
self.dependencies
.get(&block_id)
.map(|v| v.as_slice())
.unwrap_or(&[])
}
pub fn get_dependents(&self, block_id: BlockId) -> &[BlockId] {
self.dependents
.get(&block_id)
.map(|v| v.as_slice())
.unwrap_or(&[])
}
pub fn is_dependent_on(&self, dependent: BlockId, controller: BlockId) -> bool {
self.dependencies
.get(&dependent)
.map(|deps| deps.iter().any(|d| d.controller_block == controller))
.unwrap_or(false)
}
}
#[derive(Debug, Default)]
pub struct ImplicitFlowResult {
pub flows: Vec<ImplicitFlow>,
pub labels: HashMap<String, SecurityLabel>,
pub violations: Vec<ImplicitFlowViolation>,
}
#[derive(Debug, Clone)]
pub struct ImplicitFlowViolation {
pub flow: ImplicitFlow,
pub message: String,
pub severity: ViolationSeverity,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ViolationSeverity {
Info,
Warning,
Error,
Critical,
}
impl ViolationSeverity {
pub fn from_label_difference(source: SecurityLabel, target: SecurityLabel) -> Self {
let diff = (source as i32) - (target as i32);
match diff {
d if d <= 0 => ViolationSeverity::Info,
1 => ViolationSeverity::Warning,
2 => ViolationSeverity::Error,
_ => ViolationSeverity::Critical,
}
}
}
pub struct ImplicitFlowAnalyzer<'a> {
cfg: &'a CFG,
cdg: ControlDependenceGraph,
semantics: &'static LanguageSemantics,
labels: HashMap<String, SecurityLabel>,
high_patterns: Vec<String>,
tainted_vars: HashSet<String>,
}
impl<'a> ImplicitFlowAnalyzer<'a> {
pub fn new(cfg: &'a CFG, semantics: &'static LanguageSemantics) -> Self {
let cdg = ControlDependenceGraph::build(cfg);
Self {
cfg,
cdg,
semantics,
labels: HashMap::new(),
high_patterns: Self::default_high_patterns(),
tainted_vars: HashSet::new(),
}
}
fn default_high_patterns() -> Vec<String> {
vec![
"secret".to_string(),
"password".to_string(),
"passwd".to_string(),
"token".to_string(),
"key".to_string(),
"apikey".to_string(),
"api_key".to_string(),
"private".to_string(),
"credential".to_string(),
"auth".to_string(),
"ssn".to_string(),
"credit_card".to_string(),
"creditcard".to_string(),
"pin".to_string(),
]
}
pub fn set_label(&mut self, var_name: &str, label: SecurityLabel) {
self.labels.insert(var_name.to_string(), label);
}
pub fn set_tainted_vars(&mut self, tainted: HashSet<String>) {
self.tainted_vars = tainted;
}
pub fn add_high_pattern(&mut self, pattern: &str) {
self.high_patterns.push(pattern.to_lowercase());
}
fn infer_label(&self, var_name: &str) -> SecurityLabel {
if let Some(&label) = self.labels.get(var_name) {
return label;
}
if self.tainted_vars.contains(var_name) {
return SecurityLabel::Secret;
}
let lower_name = var_name.to_lowercase();
for pattern in &self.high_patterns {
if lower_name.contains(pattern) {
return SecurityLabel::Secret;
}
}
SecurityLabel::Public
}
pub fn analyze(&self, tree: &tree_sitter::Tree, source: &[u8]) -> ImplicitFlowResult {
let mut result = ImplicitFlowResult {
flows: Vec::new(),
labels: self.labels.clone(),
violations: Vec::new(),
};
for block in &self.cfg.blocks {
let deps = self.cdg.get_dependencies(block.id);
if deps.is_empty() {
continue;
}
let assignments = self.find_assignments_in_block(block, tree, source);
for (target_var, assignment_line) in assignments {
for dep in deps {
let condition_vars =
self.extract_condition_variables(dep.condition_node, tree, source);
if condition_vars.is_empty() {
continue;
}
let source_label = condition_vars
.iter()
.map(|v| self.infer_label(v))
.fold(SecurityLabel::Public, |acc, l| acc.join(l));
let target_label = self.infer_label(&target_var);
let flow = ImplicitFlow {
target_variable: target_var.clone(),
source_variables: condition_vars.clone(),
assignment_block: block.id,
condition_block: dep.controller_block,
flow_type: dep.control_type,
source_label,
target_label,
assignment_line,
condition_line: self.get_node_line(dep.condition_node, tree),
};
if flow.is_violation() {
let severity =
ViolationSeverity::from_label_difference(source_label, target_label);
let message = format!(
"Implicit flow: {} ({}) influences {} ({}) via {}",
condition_vars.join(", "),
source_label,
target_var,
target_label,
dep.control_type
);
result.violations.push(ImplicitFlowViolation {
flow: flow.clone(),
message,
severity,
});
}
result.flows.push(flow);
}
}
}
for flow in &result.flows {
for var in &flow.source_variables {
result
.labels
.entry(var.clone())
.or_insert_with(|| self.infer_label(var));
}
result
.labels
.entry(flow.target_variable.clone())
.or_insert_with(|| self.infer_label(&flow.target_variable));
}
result
}
fn find_assignments_in_block(
&self,
block: &BasicBlock,
tree: &tree_sitter::Tree,
source: &[u8],
) -> Vec<(String, Option<usize>)> {
let mut assignments = Vec::new();
for &stmt_id in &block.statements {
if let Some(node) = find_node_by_id(tree, stmt_id) {
self.collect_assignments(node, source, &mut assignments);
}
}
assignments
}
fn collect_assignments(
&self,
node: tree_sitter::Node,
source: &[u8],
assignments: &mut Vec<(String, Option<usize>)>,
) {
let kind = node.kind();
if (self.semantics.is_assignment(kind) || self.semantics.is_variable_declaration(kind))
&& let Some(var_name) = self.extract_assigned_variable(node, source)
{
let line = node.start_position().row + 1;
assignments.push((var_name, Some(line)));
}
if kind == "variable_declarator"
&& let Some(name_node) = node.child_by_field_name("name")
&& let Ok(name) = name_node.utf8_text(source)
{
let line = node.start_position().row + 1;
assignments.push((name.to_string(), Some(line)));
}
if !self.semantics.is_function_def(kind) {
let mut cursor = node.walk();
for child in node.named_children(&mut cursor) {
self.collect_assignments(child, source, assignments);
}
}
}
fn extract_assigned_variable(&self, node: tree_sitter::Node, source: &[u8]) -> Option<String> {
let target = node
.child_by_field_name(self.semantics.left_field)
.or_else(|| node.child_by_field_name("name"))
.or_else(|| node.child_by_field_name("pattern"))?;
if self.semantics.is_identifier(target.kind()) || target.kind() == "identifier" {
return target.utf8_text(source).ok().map(|s| s.to_string());
}
None
}
fn extract_condition_variables(
&self,
condition_node: Option<usize>,
tree: &tree_sitter::Tree,
source: &[u8],
) -> Vec<String> {
let node_id = match condition_node {
Some(id) => id,
None => return Vec::new(),
};
let node = match find_node_by_id(tree, node_id) {
Some(n) => n,
None => return Vec::new(),
};
let mut vars = Vec::new();
self.collect_identifiers(node, source, &mut vars);
vars
}
fn collect_identifiers(&self, node: tree_sitter::Node, source: &[u8], vars: &mut Vec<String>) {
if self.semantics.is_identifier(node.kind()) || node.kind() == "identifier" {
if let Ok(name) = node.utf8_text(source) {
if !self.is_keyword(name) {
vars.push(name.to_string());
}
}
return;
}
if self.semantics.is_function_def(node.kind()) {
return;
}
let mut cursor = node.walk();
for child in node.named_children(&mut cursor) {
self.collect_identifiers(child, source, vars);
}
}
fn is_keyword(&self, name: &str) -> bool {
matches!(
name,
"true" | "false" | "null" | "undefined" | "None" | "nil" | "True" | "False"
)
}
fn get_node_line(&self, node_id: Option<usize>, tree: &tree_sitter::Tree) -> Option<usize> {
node_id
.and_then(|id| find_node_by_id(tree, id))
.map(|n| n.start_position().row + 1)
}
pub fn control_dependence_graph(&self) -> &ControlDependenceGraph {
&self.cdg
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct LabelFact {
pub var_name: String,
pub label: SecurityLabel,
}
impl LabelFact {
pub fn new(var_name: impl Into<String>, label: SecurityLabel) -> Self {
Self {
var_name: var_name.into(),
label,
}
}
}
pub struct LabelTransfer {
pub semantics: &'static LanguageSemantics,
pub high_patterns: Vec<String>,
}
impl TransferFunction<LabelFact> for LabelTransfer {
fn transfer(
&self,
block: &BasicBlock,
input: &HashSet<LabelFact>,
_cfg: &CFG,
source: &[u8],
tree: &tree_sitter::Tree,
) -> HashSet<LabelFact> {
let mut state = input.clone();
for &stmt_id in &block.statements {
if let Some(node) = find_node_by_id(tree, stmt_id) {
self.process_statement(node, source, &mut state);
}
}
state
}
}
impl LabelTransfer {
pub fn new(semantics: &'static LanguageSemantics) -> Self {
Self {
semantics,
high_patterns: ImplicitFlowAnalyzer::default_high_patterns(),
}
}
fn process_statement(
&self,
node: tree_sitter::Node,
source: &[u8],
state: &mut HashSet<LabelFact>,
) {
let kind = node.kind();
if self.semantics.is_assignment(kind) || self.semantics.is_variable_declaration(kind) {
let target = node
.child_by_field_name(self.semantics.left_field)
.or_else(|| node.child_by_field_name("name"));
let rhs = node
.child_by_field_name(self.semantics.right_field)
.or_else(|| node.child_by_field_name(self.semantics.value_field));
if let (Some(target), Some(rhs)) = (target, rhs)
&& let Ok(var_name) = target.utf8_text(source)
{
let rhs_label = self.compute_expression_label(rhs, source, state);
state.retain(|f| f.var_name != var_name);
state.insert(LabelFact::new(var_name, rhs_label));
}
}
}
fn compute_expression_label(
&self,
node: tree_sitter::Node,
source: &[u8],
state: &HashSet<LabelFact>,
) -> SecurityLabel {
let kind = node.kind();
if (self.semantics.is_identifier(kind) || kind == "identifier")
&& let Ok(name) = node.utf8_text(source)
{
for fact in state {
if fact.var_name == name {
return fact.label;
}
}
return self.infer_label_from_name(name);
}
if self.semantics.is_literal(kind) {
return SecurityLabel::Public;
}
let mut label = SecurityLabel::Public;
let mut cursor = node.walk();
for child in node.named_children(&mut cursor) {
let child_label = self.compute_expression_label(child, source, state);
label = label.join(child_label);
}
label
}
fn infer_label_from_name(&self, name: &str) -> SecurityLabel {
let lower = name.to_lowercase();
for pattern in &self.high_patterns {
if lower.contains(pattern) {
return SecurityLabel::Secret;
}
}
SecurityLabel::Public
}
}
pub fn analyze_labels(
cfg: &CFG,
tree: &tree_sitter::Tree,
source: &[u8],
semantics: &'static LanguageSemantics,
) -> DataflowResult<LabelFact> {
let transfer = LabelTransfer::new(semantics);
super::dataflow::solve(cfg, Direction::Forward, &transfer, source, tree)
}
impl ImplicitFlowResult {
pub fn is_influenced_by_secret(&self, var_name: &str) -> bool {
self.flows
.iter()
.any(|f| f.target_variable == var_name && f.source_label.is_high())
}
pub fn influenced_by(&self, source_var: &str) -> Vec<&str> {
self.flows
.iter()
.filter(|f| f.source_variables.contains(&source_var.to_string()))
.map(|f| f.target_variable.as_str())
.collect()
}
pub fn get_violations(&self) -> &[ImplicitFlowViolation] {
&self.violations
}
pub fn has_violations(&self) -> bool {
!self.violations.is_empty()
}
pub fn get_label(&self, var_name: &str) -> SecurityLabel {
self.labels
.get(var_name)
.copied()
.unwrap_or(SecurityLabel::Public)
}
}
pub fn analyze_implicit_flows(
cfg: &CFG,
tree: &tree_sitter::Tree,
source: &[u8],
semantics: &'static LanguageSemantics,
) -> ImplicitFlowResult {
let analyzer = ImplicitFlowAnalyzer::new(cfg, semantics);
analyzer.analyze(tree, source)
}
pub fn analyze_implicit_flows_with_taint(
cfg: &CFG,
tree: &tree_sitter::Tree,
source: &[u8],
semantics: &'static LanguageSemantics,
tainted_vars: HashSet<String>,
) -> ImplicitFlowResult {
let mut analyzer = ImplicitFlowAnalyzer::new(cfg, semantics);
analyzer.set_tainted_vars(tainted_vars);
analyzer.analyze(tree, source)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::flow::cfg::CFG;
use rma_common::Language;
use rma_parser::ParserEngine;
use std::path::Path;
fn parse_js(code: &str) -> rma_parser::ParsedFile {
let config = rma_common::RmaConfig::default();
let parser = ParserEngine::new(config);
parser
.parse_file(Path::new("test.js"), code)
.expect("parse failed")
}
#[test]
fn test_security_label_ordering() {
assert!(SecurityLabel::Public < SecurityLabel::Confidential);
assert!(SecurityLabel::Confidential < SecurityLabel::Secret);
assert!(SecurityLabel::Secret < SecurityLabel::TopSecret);
assert!(SecurityLabel::Public.can_flow_to(SecurityLabel::Secret));
assert!(!SecurityLabel::Secret.can_flow_to(SecurityLabel::Public));
}
#[test]
fn test_security_label_join() {
assert_eq!(
SecurityLabel::Public.join(SecurityLabel::Secret),
SecurityLabel::Secret
);
assert_eq!(
SecurityLabel::Secret.join(SecurityLabel::Public),
SecurityLabel::Secret
);
assert_eq!(
SecurityLabel::Public.join(SecurityLabel::Public),
SecurityLabel::Public
);
}
#[test]
fn test_label_from_annotation() {
assert_eq!(
SecurityLabel::from_annotation("public"),
Some(SecurityLabel::Public)
);
assert_eq!(
SecurityLabel::from_annotation("SECRET"),
Some(SecurityLabel::Secret)
);
assert_eq!(
SecurityLabel::from_annotation("High"),
Some(SecurityLabel::Secret)
);
assert_eq!(SecurityLabel::from_annotation("unknown"), None);
}
#[test]
fn test_control_dependence_if() {
let code = r#"
if (secret) {
x = 1;
} else {
x = 0;
}
"#;
let parsed = parse_js(code);
let cfg = CFG::build(&parsed, Language::JavaScript);
let cdg = ControlDependenceGraph::build(&cfg);
assert!(!cdg.dependencies.is_empty());
}
#[test]
fn test_control_dependence_loop() {
let code = r#"
while (secret > 0) {
x = x + 1;
secret = secret - 1;
}
"#;
let parsed = parse_js(code);
let cfg = CFG::build(&parsed, Language::JavaScript);
let cdg = ControlDependenceGraph::build(&cfg);
assert!(!cdg.dependencies.is_empty());
}
#[test]
fn test_implicit_flow_detection_if() {
let code = r#"
let secret = true;
let x;
if (secret) {
x = 1;
} else {
x = 0;
}
console.log(x);
"#;
let parsed = parse_js(code);
let cfg = CFG::build(&parsed, Language::JavaScript);
let semantics = LanguageSemantics::for_language(Language::JavaScript);
let result = analyze_implicit_flows(&cfg, &parsed.tree, code.as_bytes(), semantics);
let has_flow = result.flows.iter().any(|f| {
f.source_variables.contains(&"secret".to_string()) && f.target_variable == "x"
});
assert!(has_flow, "Should detect implicit flow from secret to x");
}
#[test]
fn test_implicit_flow_with_taint() {
let code = r#"
let userInput = req.query.input;
let isAdmin;
if (userInput === "admin") {
isAdmin = true;
} else {
isAdmin = false;
}
console.log(isAdmin);
"#;
let parsed = parse_js(code);
let cfg = CFG::build(&parsed, Language::JavaScript);
let semantics = LanguageSemantics::for_language(Language::JavaScript);
let mut tainted = HashSet::new();
tainted.insert("userInput".to_string());
let result = analyze_implicit_flows_with_taint(
&cfg,
&parsed.tree,
code.as_bytes(),
semantics,
tainted,
);
let has_high_source_flow = result.flows.iter().any(|f| {
f.source_variables.contains(&"userInput".to_string()) && f.source_label.is_high()
});
assert!(
has_high_source_flow,
"Tainted variable should have high security label"
);
}
#[test]
fn test_violation_detection() {
let code = r#"
let secretKey = getSecretKey();
let publicResult;
if (secretKey > 0) {
publicResult = 1;
} else {
publicResult = 0;
}
console.log(publicResult);
"#;
let parsed = parse_js(code);
let cfg = CFG::build(&parsed, Language::JavaScript);
let semantics = LanguageSemantics::for_language(Language::JavaScript);
let mut analyzer = ImplicitFlowAnalyzer::new(&cfg, semantics);
analyzer.set_label("secretKey", SecurityLabel::Secret);
analyzer.set_label("publicResult", SecurityLabel::Public);
let result = analyzer.analyze(&parsed.tree, code.as_bytes());
assert!(
result.has_violations(),
"Should detect high-to-low implicit flow violation"
);
}
#[test]
fn test_nested_control_flow() {
let code = r#"
let secret = isAdmin();
let flag = hasPermission();
let x = 0;
if (secret) {
if (flag) {
x = 1;
} else {
x = 2;
}
}
console.log(x);
"#;
let parsed = parse_js(code);
let cfg = CFG::build(&parsed, Language::JavaScript);
let semantics = LanguageSemantics::for_language(Language::JavaScript);
let result = analyze_implicit_flows(&cfg, &parsed.tree, code.as_bytes(), semantics);
let influenced_by_condition = result.flows.iter().any(|f| {
(f.source_variables.contains(&"secret".to_string())
|| f.source_variables.contains(&"flag".to_string()))
&& f.target_variable == "x"
});
assert!(
influenced_by_condition,
"x should be influenced by conditions"
);
}
#[test]
fn test_loop_implicit_flow() {
let code = r#"
let secretCount = 10;
let result = 0;
while (secretCount > 0) {
result = result + 1;
secretCount = secretCount - 1;
}
console.log(result);
"#;
let parsed = parse_js(code);
let cfg = CFG::build(&parsed, Language::JavaScript);
let semantics = LanguageSemantics::for_language(Language::JavaScript);
let result = analyze_implicit_flows(&cfg, &parsed.tree, code.as_bytes(), semantics);
let has_loop_dep = !result.flows.is_empty() || {
let analyzer = ImplicitFlowAnalyzer::new(&cfg, semantics);
let cdg = analyzer.control_dependence_graph();
cdg.dependencies.values().any(|deps| {
deps.iter()
.any(|d| d.control_type == ImplicitFlowType::LoopCondition)
})
};
assert!(has_loop_dep, "Should detect control dependence in loop");
}
#[test]
fn test_switch_implicit_flow() {
let code = r#"
let secretType = getSecretType();
let result;
switch (secretType) {
case 1:
result = "a";
break;
case 2:
result = "b";
break;
default:
result = "c";
}
console.log(result);
"#;
let parsed = parse_js(code);
let cfg = CFG::build(&parsed, Language::JavaScript);
let semantics = LanguageSemantics::for_language(Language::JavaScript);
let result = analyze_implicit_flows(&cfg, &parsed.tree, code.as_bytes(), semantics);
let has_switch_flow = result
.flows
.iter()
.any(|f| f.flow_type == ImplicitFlowType::SwitchCase);
assert!(
has_switch_flow,
"Should detect implicit flow through switch"
);
}
#[test]
fn test_label_inference_from_name() {
let code = r#"
const password = "secret123";
const apiKey = getEnv("KEY");
const normalVar = 42;
"#;
let parsed = parse_js(code);
let cfg = CFG::build(&parsed, Language::JavaScript);
let semantics = LanguageSemantics::for_language(Language::JavaScript);
let analyzer = ImplicitFlowAnalyzer::new(&cfg, semantics);
assert!(analyzer.infer_label("password").is_high());
assert!(analyzer.infer_label("apiKey").is_high());
assert!(analyzer.infer_label("normalVar").is_low());
}
#[test]
fn test_label_propagation() {
let code = r#"
const password = "hunter2";
const x = password;
const y = x;
"#;
let parsed = parse_js(code);
let cfg = CFG::build(&parsed, Language::JavaScript);
let semantics = LanguageSemantics::for_language(Language::JavaScript);
let result = analyze_labels(&cfg, &parsed.tree, code.as_bytes(), semantics);
let _has_high_label = result.block_exit.values().any(|set| {
set.iter()
.any(|f| f.var_name == "password" && f.label.is_high())
}) || result.block_entry.values().any(|set| {
set.iter()
.any(|f| f.var_name == "password" && f.label.is_high())
});
let analysis_ran = result.iterations > 0 || !result.block_exit.is_empty();
assert!(analysis_ran, "Label propagation analysis should have run");
let transfer = LabelTransfer::new(semantics);
assert!(
transfer
.high_patterns
.iter()
.any(|p| "password".contains(p)),
"password should match high-security pattern"
);
}
#[test]
fn test_implicit_flow_result_queries() {
let code = r#"
let secretData = getSecret();
let x;
if (secretData) {
x = 1;
}
console.log(x);
"#;
let parsed = parse_js(code);
let cfg = CFG::build(&parsed, Language::JavaScript);
let semantics = LanguageSemantics::for_language(Language::JavaScript);
let result = analyze_implicit_flows(&cfg, &parsed.tree, code.as_bytes(), semantics);
let influenced = result.influenced_by("secretData");
let _ = influenced;
let label = result.get_label("unknownVar");
assert_eq!(label, SecurityLabel::Public); }
}