use std::fmt;
use std::ops::Deref;
use chrono::{DateTime, NaiveDateTime, Utc};
use serde_json::{Map, Value};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Category {
Humanity,
Authenticity,
Uniqueness,
Behavior,
}
impl Category {
pub fn as_str(self) -> &'static str {
match self {
Category::Humanity => "HUMANITY",
Category::Authenticity => "AUTHENTICITY",
Category::Uniqueness => "UNIQUENESS",
Category::Behavior => "BEHAVIOR",
}
}
pub fn parse(value: &str) -> Option<Category> {
[
Category::Humanity,
Category::Authenticity,
Category::Uniqueness,
Category::Behavior,
]
.into_iter()
.find(|category| category.as_str().eq_ignore_ascii_case(value))
}
}
impl fmt::Display for Category {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct TrackResult {
pub status: Option<String>,
pub id: Option<String>,
pub fingerprint: Option<String>,
pub raw: Value,
}
impl TrackResult {
pub fn accepted(&self) -> bool {
self.id.is_some()
}
pub(crate) fn from_value(raw: Value) -> Self {
TrackResult {
status: string(&raw, "status"),
id: string(&raw, "id"),
fingerprint: string(&raw, "fingerprint"),
raw,
}
}
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct Badge {
pub slug: Option<String>,
pub name: Option<String>,
pub badge_type: Option<String>,
pub explanation: Option<String>,
pub metadata: Map<String, Value>,
pub raw: Value,
}
impl Badge {
fn from_value(raw: &Value) -> Self {
Badge {
slug: string(raw, "slug"),
name: string(raw, "name"),
badge_type: string(raw, "type"),
explanation: string(raw, "explanation"),
metadata: object(raw, "metadata"),
raw: raw.clone(),
}
}
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct Observation {
pub category: Option<Category>,
pub id: Option<String>,
pub label: Option<String>,
pub explanation: Option<String>,
pub value: Option<f64>,
pub confidence: Option<f64>,
pub weight: Option<f64>,
pub metadata: Map<String, Value>,
pub raw: Value,
}
impl Observation {
fn from_value(raw: &Value) -> Self {
Observation {
category: category(raw),
id: string(raw, "id"),
label: string(raw, "label"),
explanation: string(raw, "explanation"),
value: float(raw, "value"),
confidence: float(raw, "confidence"),
weight: float(raw, "weight"),
metadata: object(raw, "metadata"),
raw: raw.clone(),
}
}
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct Score {
pub category: Option<Category>,
pub value: Option<i64>,
pub observations: Vec<Observation>,
pub raw: Value,
}
impl Score {
fn from_value(raw: &Value) -> Self {
Score {
category: category(raw),
value: integer(raw, "value"),
observations: objects(raw, "observations")
.map(Observation::from_value)
.collect(),
raw: raw.clone(),
}
}
fn bare(category: Category, value: i64) -> Self {
Score {
category: Some(category),
value: Some(value),
observations: Vec::new(),
raw: Value::Null,
}
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct Scores(Vec<Score>);
impl Scores {
pub fn get(&self, category: Category) -> Option<&Score> {
self.0.iter().find(|score| score.category == Some(category))
}
fn value(&self, category: Category) -> Option<i64> {
self.get(category).and_then(|score| score.value)
}
pub fn humanity(&self) -> Option<i64> {
self.value(Category::Humanity)
}
pub fn authenticity(&self) -> Option<i64> {
self.value(Category::Authenticity)
}
pub fn uniqueness(&self) -> Option<i64> {
self.value(Category::Uniqueness)
}
pub fn behavior(&self) -> Option<i64> {
self.value(Category::Behavior)
}
pub fn into_vec(self) -> Vec<Score> {
self.0
}
pub(crate) fn from_value(raw: &Value) -> Self {
let items = match raw {
Value::Array(items) => items
.iter()
.filter(|item| item.is_object())
.map(Score::from_value)
.collect(),
_ => Vec::new(),
};
Scores(items)
}
}
impl Deref for Scores {
type Target = [Score];
fn deref(&self) -> &[Score] {
&self.0
}
}
impl IntoIterator for Scores {
type Item = Score;
type IntoIter = std::vec::IntoIter<Score>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
impl<'a> IntoIterator for &'a Scores {
type Item = &'a Score;
type IntoIter = std::slice::Iter<'a, Score>;
fn into_iter(self) -> Self::IntoIter {
self.0.iter()
}
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct Identity {
pub id: Option<String>,
pub display_name: Option<String>,
pub display_email: Option<String>,
pub display_username: Option<String>,
pub humanity_score: Option<i64>,
pub authenticity_score: Option<i64>,
pub uniqueness_score: Option<i64>,
pub behavior_score: Option<i64>,
pub created_at: Option<DateTime<Utc>>,
pub updated_at: Option<DateTime<Utc>>,
pub last_tracked_at: Option<DateTime<Utc>>,
pub last_scored_at: Option<DateTime<Utc>>,
pub disregarded: bool,
pub badges: Vec<Badge>,
pub data: Map<String, Value>,
pub raw: Value,
}
impl Identity {
pub fn scores(&self) -> Scores {
let pairs = [
(Category::Humanity, self.humanity_score),
(Category::Authenticity, self.authenticity_score),
(Category::Uniqueness, self.uniqueness_score),
(Category::Behavior, self.behavior_score),
];
Scores(
pairs
.into_iter()
.filter_map(|(category, value)| value.map(|value| Score::bare(category, value)))
.collect(),
)
}
pub(crate) fn from_value(raw: Value) -> Self {
Identity {
id: string(&raw, "id"),
display_name: string(&raw, "displayName"),
display_email: string(&raw, "displayEmail"),
display_username: string(&raw, "displayUsername"),
humanity_score: integer(&raw, "humanityScore"),
authenticity_score: integer(&raw, "authenticityScore"),
uniqueness_score: integer(&raw, "uniquenessScore"),
behavior_score: integer(&raw, "behaviorScore"),
created_at: datetime(&raw, "createdAt"),
updated_at: datetime(&raw, "updatedAt"),
last_tracked_at: datetime(&raw, "lastTrackedAt"),
last_scored_at: datetime(&raw, "lastScoredAt"),
disregarded: boolean_or(&raw, "disregarded", false),
badges: objects(&raw, "badges").map(Badge::from_value).collect(),
data: object(&raw, "data"),
raw,
}
}
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct Analysis {
pub id: Option<i64>,
pub identity_id: Option<String>,
pub scores: Scores,
pub event_count: Option<i64>,
pub device_count: Option<i64>,
pub duration_millis: Option<i64>,
pub started_at: Option<DateTime<Utc>>,
pub finished_at: Option<DateTime<Utc>>,
pub raw: Value,
}
impl Analysis {
pub fn observations(&self) -> impl Iterator<Item = &Observation> {
self.scores
.iter()
.flat_map(|score| score.observations.iter())
}
pub(crate) fn from_value(raw: Value) -> Self {
Analysis {
id: integer(&raw, "id"),
identity_id: string(&raw, "identityId"),
scores: raw
.get("scores")
.map(Scores::from_value)
.unwrap_or_default(),
event_count: integer(&raw, "eventCount"),
device_count: integer(&raw, "deviceCount"),
duration_millis: integer(&raw, "durationMillis"),
started_at: datetime(&raw, "startedAt"),
finished_at: datetime(&raw, "finishedAt"),
raw,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum EscalationStatus {
Open,
Acknowledged,
Closed,
}
impl EscalationStatus {
pub fn as_str(self) -> &'static str {
match self {
EscalationStatus::Open => "OPEN",
EscalationStatus::Acknowledged => "ACKNOWLEDGED",
EscalationStatus::Closed => "CLOSED",
}
}
pub fn parse(value: &str) -> Option<EscalationStatus> {
[
EscalationStatus::Open,
EscalationStatus::Acknowledged,
EscalationStatus::Closed,
]
.into_iter()
.find(|status| status.as_str().eq_ignore_ascii_case(value))
}
}
impl fmt::Display for EscalationStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum EscalationSeverity {
Info,
Warning,
Critical,
}
impl EscalationSeverity {
pub fn as_str(self) -> &'static str {
match self {
EscalationSeverity::Info => "INFO",
EscalationSeverity::Warning => "WARNING",
EscalationSeverity::Critical => "CRITICAL",
}
}
pub fn parse(value: &str) -> Option<EscalationSeverity> {
[
EscalationSeverity::Info,
EscalationSeverity::Warning,
EscalationSeverity::Critical,
]
.into_iter()
.find(|severity| severity.as_str().eq_ignore_ascii_case(value))
}
}
impl fmt::Display for EscalationSeverity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct Reference {
pub id: Option<String>,
pub label: Option<String>,
pub path: Option<String>,
pub raw: Value,
}
impl Reference {
fn from_value(raw: &Value) -> Self {
Reference {
id: id(raw, "id"),
label: string(raw, "label"),
path: string(raw, "path"),
raw: raw.clone(),
}
}
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct GroupReference {
pub group_type: Option<String>,
pub id: Option<String>,
pub label: Option<String>,
pub path: Option<String>,
pub raw: Value,
}
impl GroupReference {
fn from_value(raw: &Value) -> Self {
GroupReference {
group_type: string(raw, "type"),
id: id(raw, "id"),
label: string(raw, "label"),
path: string(raw, "path"),
raw: raw.clone(),
}
}
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct TrackedGroup {
pub group_type: Option<String>,
pub id: Option<String>,
pub display_name: Option<String>,
pub identity_count: Option<i64>,
pub created_at: Option<DateTime<Utc>>,
pub updated_at: Option<DateTime<Utc>>,
pub last_tracked_at: Option<DateTime<Utc>>,
pub data: Map<String, Value>,
pub raw: Value,
}
impl TrackedGroup {
pub(crate) fn from_value(raw: Value) -> Self {
TrackedGroup {
group_type: string(&raw, "type"),
id: id(&raw, "id"),
display_name: string(&raw, "displayName"),
identity_count: integer(&raw, "identityCount"),
created_at: datetime(&raw, "createdAt"),
updated_at: datetime(&raw, "updatedAt"),
last_tracked_at: datetime(&raw, "lastTrackedAt"),
data: object(&raw, "data"),
raw,
}
}
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct TrackedEvent {
pub id: Option<String>,
pub timestamp: Option<DateTime<Utc>>,
pub identity: Option<Reference>,
pub groups: Vec<GroupReference>,
pub fingerprint: Option<String>,
pub session: Option<String>,
pub ip: Option<String>,
pub url: Option<String>,
pub source: Option<String>,
pub name: Option<String>,
pub canonical_type: Option<String>,
pub data: Map<String, Value>,
pub raw: Value,
}
impl TrackedEvent {
pub(crate) fn from_value(raw: Value) -> Self {
TrackedEvent {
id: id(&raw, "id"),
timestamp: datetime(&raw, "timestamp"),
identity: nested(&raw, "identity", Reference::from_value),
groups: objects(&raw, "groups")
.map(GroupReference::from_value)
.collect(),
fingerprint: string(&raw, "fingerprint"),
session: string(&raw, "session"),
ip: string(&raw, "ip"),
url: string(&raw, "url"),
source: string(&raw, "eventSource"),
name: string(&raw, "eventName"),
canonical_type: string(&raw, "canonicalType"),
data: object(&raw, "eventData"),
raw,
}
}
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct Device {
pub fingerprint: Option<String>,
pub ip: Option<String>,
pub asn: Option<String>,
pub asn_label: Option<String>,
pub user_agent: Option<String>,
pub city: Option<String>,
pub region: Option<String>,
pub country: Option<String>,
pub country_code: Option<String>,
pub timezone: Option<String>,
pub latitude: Option<f64>,
pub longitude: Option<f64>,
pub disregarded: bool,
pub first_seen_at: Option<DateTime<Utc>>,
pub last_seen_at: Option<DateTime<Utc>>,
pub raw: Value,
}
impl Device {
pub(crate) fn from_value(raw: Value) -> Self {
Device {
fingerprint: string(&raw, "fingerprint"),
ip: string(&raw, "ip"),
asn: string(&raw, "asn"),
asn_label: string(&raw, "asnLabel"),
user_agent: string(&raw, "userAgent"),
city: string(&raw, "city"),
region: string(&raw, "region"),
country: string(&raw, "country"),
country_code: string(&raw, "countryCode"),
timezone: string(&raw, "timezone"),
latitude: float(&raw, "latitude"),
longitude: float(&raw, "longitude"),
disregarded: boolean_or(&raw, "disregarded", false),
first_seen_at: datetime(&raw, "firstSeenAt"),
last_seen_at: datetime(&raw, "lastSeenAt"),
raw,
}
}
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct LinkedIdentity {
pub identity_id: Option<String>,
pub display_name: Option<String>,
pub link_type: Option<String>,
pub value: Option<f64>,
pub confidence: Option<f64>,
pub explanation: Option<String>,
pub metadata: Map<String, Value>,
pub created_at: Option<DateTime<Utc>>,
pub raw: Value,
}
impl LinkedIdentity {
pub(crate) fn from_value(raw: Value) -> Self {
LinkedIdentity {
identity_id: string(&raw, "targetIdentityId"),
display_name: string(&raw, "targetDisplayName"),
link_type: string(&raw, "type"),
value: float(&raw, "value"),
confidence: float(&raw, "confidence"),
explanation: string(&raw, "explanation"),
metadata: object(&raw, "metadata"),
created_at: datetime(&raw, "createdAt"),
raw,
}
}
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct ScoreHistoryEntry {
pub cycle_id: Option<i64>,
pub humanity: Option<i64>,
pub authenticity: Option<i64>,
pub uniqueness: Option<i64>,
pub behavior: Option<i64>,
pub event_count: Option<i64>,
pub device_count: Option<i64>,
pub finished_at: Option<DateTime<Utc>>,
pub raw: Value,
}
impl ScoreHistoryEntry {
pub(crate) fn from_value(raw: Value) -> Self {
ScoreHistoryEntry {
cycle_id: integer(&raw, "id"),
humanity: integer(&raw, "humanityScore"),
authenticity: integer(&raw, "authenticityScore"),
uniqueness: integer(&raw, "uniquenessScore"),
behavior: integer(&raw, "behaviorScore"),
event_count: integer(&raw, "eventCount"),
device_count: integer(&raw, "deviceCount"),
finished_at: datetime(&raw, "finishedAt"),
raw,
}
}
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct IdentityReview {
pub id: Option<i64>,
pub identity_id: Option<String>,
pub status: Option<String>,
pub feedback: Option<String>,
pub summary: Option<String>,
pub error_message: Option<String>,
pub created_at: Option<DateTime<Utc>>,
pub completed_at: Option<DateTime<Utc>>,
pub raw: Value,
}
impl IdentityReview {
pub(crate) fn from_value(raw: Value) -> Self {
IdentityReview {
id: integer(&raw, "id"),
identity_id: string(&raw, "identityId"),
status: string(&raw, "status"),
feedback: string(&raw, "userFeedback"),
summary: string(&raw, "summary"),
error_message: string(&raw, "errorMessage"),
created_at: datetime(&raw, "createdAt"),
completed_at: datetime(&raw, "completedAt"),
raw,
}
}
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct Escalation {
pub id: Option<i64>,
pub status: Option<EscalationStatus>,
pub severity: Option<EscalationSeverity>,
pub identity: Option<Reference>,
pub rule: Option<Reference>,
pub scores: Map<String, Value>,
pub opened_at: Option<DateTime<Utc>>,
pub acknowledged_at: Option<DateTime<Utc>>,
pub closed_at: Option<DateTime<Utc>>,
pub created_at: Option<DateTime<Utc>>,
pub raw: Value,
}
impl Escalation {
pub(crate) fn from_value(raw: Value) -> Self {
Escalation {
id: integer(&raw, "id"),
status: raw
.get("status")
.and_then(Value::as_str)
.and_then(EscalationStatus::parse),
severity: raw
.get("severity")
.and_then(Value::as_str)
.and_then(EscalationSeverity::parse),
identity: nested(&raw, "identity", Reference::from_value),
rule: nested(&raw, "escalationRule", Reference::from_value),
scores: object(&raw, "scoreSnapshot"),
opened_at: datetime(&raw, "openedAt"),
acknowledged_at: datetime(&raw, "acknowledgedAt"),
closed_at: datetime(&raw, "closedAt"),
created_at: datetime(&raw, "createdAt"),
raw,
}
}
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct EscalationSummary {
pub open: u64,
pub acknowledged: u64,
pub closed: u64,
pub raw: Value,
}
impl EscalationSummary {
pub(crate) fn from_value(raw: Value) -> Self {
let count = |key| {
integer(&raw, key)
.and_then(|value| u64::try_from(value).ok())
.unwrap_or(0)
};
EscalationSummary {
open: count("open"),
acknowledged: count("acknowledged"),
closed: count("closed"),
raw,
}
}
}
fn id(raw: &Value, key: &str) -> Option<String> {
match raw.get(key)? {
Value::String(value) => Some(value.clone()),
Value::Number(value) => Some(value.to_string()),
_ => None,
}
}
fn string(raw: &Value, key: &str) -> Option<String> {
raw.get(key).and_then(Value::as_str).map(str::to_owned)
}
fn float(raw: &Value, key: &str) -> Option<f64> {
raw.get(key).and_then(Value::as_f64)
}
pub(crate) fn integer(raw: &Value, key: &str) -> Option<i64> {
let value = raw.get(key)?;
value.as_i64().or_else(|| {
value
.as_f64()
.filter(|f| f.fract() == 0.0 && f.is_finite())
.map(|f| f as i64)
})
}
pub(crate) fn boolean_or(raw: &Value, key: &str, default: bool) -> bool {
raw.get(key).and_then(Value::as_bool).unwrap_or(default)
}
fn nested<T>(raw: &Value, key: &str, parse: fn(&Value) -> T) -> Option<T> {
raw.get(key).filter(|value| value.is_object()).map(parse)
}
fn object(raw: &Value, key: &str) -> Map<String, Value> {
raw.get(key)
.and_then(Value::as_object)
.cloned()
.unwrap_or_default()
}
fn objects<'a>(raw: &'a Value, key: &str) -> impl Iterator<Item = &'a Value> {
raw.get(key)
.and_then(Value::as_array)
.into_iter()
.flatten()
.filter(|item| item.is_object())
}
fn category(raw: &Value) -> Option<Category> {
raw.get("category")
.and_then(Value::as_str)
.and_then(Category::parse)
}
pub(crate) fn parse_datetime(value: &str) -> Option<DateTime<Utc>> {
if let Ok(parsed) = DateTime::parse_from_rfc3339(value) {
return Some(parsed.with_timezone(&Utc));
}
NaiveDateTime::parse_from_str(value, "%Y-%m-%dT%H:%M:%S%.f")
.ok()
.map(|naive| naive.and_utc())
}
fn datetime(raw: &Value, key: &str) -> Option<DateTime<Utc>> {
raw.get(key)
.and_then(Value::as_str)
.and_then(parse_datetime)
}