dragonfly_client_rs/client/
models.rs1use color_eyre::Result;
2use serde::Serialize;
3use serde::{self, Deserialize};
4use std::collections::HashMap;
5use std::fmt::Display;
6use yara::{Compiler, Rules, ScanFlags};
7
8pub type ScanResult = Result<SubmitJobResultsSuccess, SubmitJobResultsError>;
9
10#[derive(Serialize, Debug)]
11#[serde(untagged)]
12#[serde(remote = "ScanResult")]
13enum ScanResultDef {
14 Ok(SubmitJobResultsSuccess),
15 Err(SubmitJobResultsError),
16}
17
18#[derive(Serialize)]
19pub struct ScanResultSerializer(#[serde(with = "ScanResultDef")] ScanResult);
20
21impl From<ScanResult> for ScanResultSerializer {
22 fn from(value: ScanResult) -> Self {
23 Self(value)
24 }
25}
26
27#[derive(Debug, Serialize, PartialEq)]
28pub struct SubmitJobResultsSuccess {
29 pub name: String,
30 pub version: String,
31 pub attempt: u64,
32 pub assignment_id: String,
33 pub score: i64,
34 pub inspector_url: Option<String>,
35
36 pub rules_matched: Vec<String>,
38
39 pub commit: String,
41}
42
43#[derive(Debug, Serialize, PartialEq)]
44pub struct SubmitJobResultsError {
45 pub name: String,
46 pub version: String,
47 pub attempt: u64,
48 pub assignment_id: String,
49 pub reason: String,
50}
51
52impl Display for SubmitJobResultsError {
53 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54 writeln!(f, "Name: {}", self.name)?;
55 writeln!(f, "Version: {}", self.version)?;
56 writeln!(f, "Reason: {}", self.reason)?;
57
58 Ok(())
59 }
60}
61
62#[derive(Debug, Deserialize, PartialEq)]
63pub struct Job {
64 pub hash: String,
65 pub name: String,
66 pub version: String,
67 pub distributions: Vec<String>,
68 pub attempt: u64,
69 pub assignment_id: String,
70}
71
72#[derive(Debug, Deserialize)]
73pub struct RulesResponse {
74 pub hash: String,
75 pub rules: HashMap<String, String>,
76}
77
78impl RulesResponse {
79 pub fn compile(&self) -> Result<Rules> {
81 let mut sources = self.rules.iter().collect::<Vec<_>>();
82 sources.sort_unstable_by_key(|(name, _)| *name);
83 let rules_str = sources
84 .into_iter()
85 .map(|(_, source)| source.as_str())
86 .collect::<Vec<_>>()
87 .join("\n");
88
89 let mut compiled_rules = Compiler::new()?
90 .add_rules_str(&rules_str)?
91 .compile_rules()?;
92 compiled_rules.set_flags(ScanFlags::FAST_MODE);
93
94 Ok(compiled_rules)
95 }
96}
97
98#[cfg(test)]
99mod tests {
100 use super::Job;
101
102 #[test]
103 fn independently_loaded_rules_compile_in_the_same_order() {
104 for _ in 0..16 {
105 let response: super::RulesResponse = serde_json::from_str(
106 r#"{
107 "hash":"same", "rules":{
108 "z":"rule last { condition: true }",
109 "a":"rule first { condition: true }"
110 }
111 }"#,
112 )
113 .unwrap();
114 let rules = response.compile().unwrap();
115 let matches = rules.scan_mem(b"same bytes", 10).unwrap();
116 assert_eq!(
117 matches.iter().map(|r| r.identifier).collect::<Vec<_>>(),
118 vec!["first", "last"]
119 );
120 }
121 }
122
123 #[test]
124 fn job_deserializes_assignment_lease() {
125 let job: Job = serde_json::from_str(
126 r#"{
127 "hash": "rules-commit",
128 "name": "example",
129 "version": "1.2.3",
130 "distributions": ["https://example.com/example.whl"],
131 "attempt": 2,
132 "assignment_id": "4e3702e8-27a3-46e6-b51c-4779a94fa4ab"
133 }"#,
134 )
135 .unwrap();
136
137 assert_eq!(job.attempt, 2);
138 assert_eq!(job.assignment_id, "4e3702e8-27a3-46e6-b51c-4779a94fa4ab");
139 }
140
141 #[test]
142 fn job_requires_assignment_lease() {
143 let result = serde_json::from_str::<Job>(
144 r#"{
145 "hash": "rules-commit",
146 "name": "example",
147 "version": "1.2.3",
148 "distributions": []
149 }"#,
150 );
151
152 assert!(result.is_err());
153 }
154}