use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ValueFlow {
pub taint: Taint,
pub constant: Option<ConstantValue>,
pub value_set: ValueSet,
pub string_shape: Option<StringShape>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Taint {
pub kinds: Vec<TaintKind>,
pub cleansed_by: Vec<TaintCleanser>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TaintKind {
UserInput,
BindVariable,
DynamicSql,
DbLink,
FileSystem,
Network,
Environment,
SchedulerArgument,
Unanalyzable,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TaintCleanser {
DbmsAssert,
HexEncode,
LiteralOnly,
OutputSink,
OperatorAttested,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum ConstantValue {
Int { value: String },
Float { value: String },
Str { value: String },
Bool { value: bool },
Null,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum ValueSet {
#[default]
Top,
OneOf { values: Vec<ConstantValue> },
Range {
lo: ConstantValue,
hi: ConstantValue,
},
Bottom,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum StringShape {
Literal { value: String },
InterpolatedWithFix {
literal_prefix: String,
literal_suffix: String,
},
FullyOpaque,
Empty,
}
impl Taint {
#[must_use]
pub fn flags_alarm(&self) -> bool {
!self.kinds.is_empty()
}
}
impl ValueSet {
#[must_use]
pub fn join(self, other: ValueSet) -> ValueSet {
match (self, other) {
(ValueSet::Top, _) | (_, ValueSet::Top) => ValueSet::Top,
(ValueSet::Bottom, x) | (x, ValueSet::Bottom) => x,
(ValueSet::OneOf { mut values }, ValueSet::OneOf { values: other }) => {
for v in other {
if !values.contains(&v) {
values.push(v);
}
}
ValueSet::OneOf { values }
}
_ => ValueSet::Top,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn taint_flags_alarm_when_no_cleanser() {
let t = Taint {
kinds: vec![TaintKind::UserInput],
cleansed_by: vec![],
};
assert!(t.flags_alarm());
}
#[test]
fn taint_does_not_flag_when_cleansed() {
let t = Taint {
kinds: vec![],
cleansed_by: vec![TaintCleanser::DbmsAssert],
};
assert!(!t.flags_alarm());
}
#[test]
fn taint_flags_when_live_kind_present_despite_a_recorded_cleanser() {
let t = Taint {
kinds: vec![TaintKind::UserInput],
cleansed_by: vec![TaintCleanser::DbmsAssert],
};
assert!(t.flags_alarm());
}
#[test]
fn taint_default_no_alarm() {
assert!(!Taint::default().flags_alarm());
}
#[test]
fn value_set_top_dominates_join() {
let a = ValueSet::Top;
let b = ValueSet::OneOf {
values: vec![ConstantValue::Int { value: "1".into() }],
};
assert!(matches!(a.join(b), ValueSet::Top));
}
#[test]
fn value_set_bottom_yields_other_side() {
let a = ValueSet::Bottom;
let b = ValueSet::OneOf {
values: vec![ConstantValue::Int { value: "7".into() }],
};
match a.join(b) {
ValueSet::OneOf { values } => {
assert_eq!(values.len(), 1);
}
_ => panic!(),
}
}
#[test]
fn one_of_join_unions_values_dedup() {
let a = ValueSet::OneOf {
values: vec![
ConstantValue::Int { value: "1".into() },
ConstantValue::Int { value: "2".into() },
],
};
let b = ValueSet::OneOf {
values: vec![
ConstantValue::Int { value: "2".into() },
ConstantValue::Int { value: "3".into() },
],
};
match a.join(b) {
ValueSet::OneOf { values } => {
assert_eq!(values.len(), 3);
}
_ => panic!(),
}
}
#[test]
fn range_plus_one_of_widens_to_top() {
let a = ValueSet::Range {
lo: ConstantValue::Int { value: "0".into() },
hi: ConstantValue::Int { value: "10".into() },
};
let b = ValueSet::OneOf {
values: vec![ConstantValue::Int { value: "5".into() }],
};
assert!(matches!(a.join(b), ValueSet::Top));
}
#[test]
fn string_shape_variants_serialise_snake_case() {
let lit = StringShape::Literal {
value: "hello".into(),
};
let json = serde_json::to_string(&lit).unwrap();
assert!(json.contains("\"kind\":\"literal\""));
let opaque = StringShape::FullyOpaque;
assert!(
serde_json::to_string(&opaque)
.unwrap()
.contains("\"fully_opaque\"")
);
}
#[test]
fn value_flow_default_is_top_no_taint_no_constant() {
let v = ValueFlow::default();
assert!(matches!(v.value_set, ValueSet::Top));
assert!(v.constant.is_none());
assert!(v.string_shape.is_none());
assert!(v.taint.kinds.is_empty());
}
#[test]
fn value_flow_serde_round_trip() {
let v = ValueFlow {
taint: Taint {
kinds: vec![TaintKind::UserInput, TaintKind::DynamicSql],
cleansed_by: vec![TaintCleanser::DbmsAssert],
},
constant: Some(ConstantValue::Str {
value: "hello".into(),
}),
value_set: ValueSet::OneOf {
values: vec![ConstantValue::Int { value: "1".into() }],
},
string_shape: Some(StringShape::InterpolatedWithFix {
literal_prefix: "SELECT * FROM ".into(),
literal_suffix: " WHERE id = 1".into(),
}),
};
let json = serde_json::to_string(&v).unwrap();
let back: ValueFlow = serde_json::from_str(&json).unwrap();
assert_eq!(back, v);
assert!(json.contains("\"user_input\""));
assert!(json.contains("\"dbms_assert\""));
}
}