dragonfly_client_rs/
main.rs1mod app_config;
2mod client;
3mod durable_cache;
4mod exts;
5mod reuse_cache;
6mod scan_cache;
7mod scanner;
8mod utils;
9
10use std::time::{Duration, Instant};
11
12use client::DragonflyClient;
13use color_eyre::eyre::{ensure, Result};
14use tracing::{error, info, span, warn, Level};
15use tracing_subscriber::EnvFilter;
16
17use crate::{
18 app_config::APP_CONFIG,
19 client::{Job, ScanResult, SubmitJobResultsError},
20 scanner::{scan_all_distributions, PackageScanResults},
21};
22
23fn scan_package(
24 client: &DragonflyClient,
25 job: &Job,
26 stats: &mut crate::reuse_cache::CacheStats,
27) -> Option<ScanResult> {
28 if job.hash != client.rules_state.hash {
29 warn!(
30 event = "scan_deferred",
31 required_rules_commit = %job.hash,
32 loaded_rules_commit = %client.rules_state.hash,
33 "Deferring job requiring rules commit {}; scanner loaded {}",
34 job.hash, client.rules_state.hash
35 );
36 return None;
38 }
39
40 info!(
41 event = "scan_started",
42 distribution_count = job.distributions.len(),
43 rules_commit = %job.hash,
44 "Started package scan"
45 );
46
47 let result = match scan_all_distributions(
48 client.download_client(),
49 &client.rules_state.rules,
50 job,
51 Some(&client.reuse_cache),
52 stats,
53 ) {
54 Ok(results) => {
55 let package_scan_results = PackageScanResults::new(
56 job.name.clone(),
57 job.version.clone(),
58 job.attempt,
59 job.assignment_id.clone(),
60 results,
61 job.hash.clone(),
62 );
63 let body = package_scan_results.build_body();
64
65 Ok(body)
66 }
67 Err(err) => Err(SubmitJobResultsError {
68 name: job.name.clone(),
69 version: job.version.clone(),
70 attempt: job.attempt,
71 assignment_id: job.assignment_id.clone(),
72 reason: format!("{err}"),
73 }),
74 };
75 Some(result)
76}
77
78fn run_job(client: &DragonflyClient, job: &Job) {
79 let span = span!(
80 Level::INFO,
81 "scan_job",
82 package = %job.name,
83 version = %job.version,
84 attempt = job.attempt,
85 assignment_id = %job.assignment_id,
86 rules_commit = %job.hash,
87 );
88 let _enter = span.enter();
89 let started_at = Instant::now();
90
91 client.reuse_cache.begin_job(job);
92 let mut stats = crate::reuse_cache::CacheStats::new("yara", client.reuse_cache.mode);
93 let Some(scan_result) = scan_package(client, job, &mut stats) else {
94 return;
95 };
96
97 stats.emit();
98 let outcome = match &scan_result {
99 Ok(result) => {
100 info!(
101 event = "scan_completed",
102 elapsed_ms = started_at.elapsed().as_millis(),
103 distribution_count = job.distributions.len(),
104 score = result.score,
105 matched_rule_count = result.rules_matched.len(),
106 "Completed package scan"
107 );
108 "finished"
109 }
110 Err(result) => {
111 error!(
112 event = "scan_failed",
113 elapsed_ms = started_at.elapsed().as_millis(),
114 distribution_count = job.distributions.len(),
115 reason = %result.reason,
116 "Package scan failed"
117 );
118 "failed"
119 }
120 };
121
122 match client.send_result(scan_result, &stats) {
123 Ok(()) => info!(
124 event = "result_submitted",
125 elapsed_ms = started_at.elapsed().as_millis(),
126 outcome,
127 "Submitted package scan result"
128 ),
129 Err(err) => {
130 error!(
131 event = "result_submission_failed",
132 elapsed_ms = started_at.elapsed().as_millis(),
133 outcome,
134 error = %err,
135 "Error while sending package scan result to API"
136 );
137 }
138 }
139}
140
141fn run_jobs(client: &DragonflyClient, jobs: Vec<Job>, worker_count: usize) {
142 if worker_count == 1 {
143 for job in jobs {
144 run_job(client, &job);
145 }
146 return;
147 }
148
149 std::thread::scope(|scope| {
150 for job in jobs {
151 scope.spawn(move || run_job(client, &job));
152 }
153 });
154}
155
156fn main() -> Result<()> {
157 color_eyre::install()?;
158
159 let default_env_filter = EnvFilter::builder()
160 .parse("warn,dragonfly_client_rs=info")
161 .unwrap();
162 let env_filter = EnvFilter::try_from_default_env().unwrap_or(default_env_filter);
163
164 tracing_subscriber::fmt().with_env_filter(env_filter).init();
165 let mut client = DragonflyClient::new()?;
166 ensure!(
167 !APP_CONFIG.reuse_cache_database || APP_CONFIG.threads == 1,
168 "Database cache requires one scan thread"
169 );
170 ensure!(APP_CONFIG.reuse_cache_mode != crate::reuse_cache::CacheMode::Reuse || APP_CONFIG.threads == 1,
171 "Cross-package reuse requires DRAGONFLY_THREADS=1 so validation can invalidate the entire active job");
172 ensure!(
173 APP_CONFIG.threads > 0,
174 "DRAGONFLY_THREADS must be greater than zero"
175 );
176 ensure!(
177 APP_CONFIG.bulk_size > 0,
178 "DRAGONFLY_BULK_SIZE must be greater than zero"
179 );
180 let batch_size = APP_CONFIG.bulk_size.min(APP_CONFIG.threads);
181
182 loop {
183 info!("Fetching up to {batch_size} jobs");
184 match client.bulk_get_job(batch_size) {
185 Ok(jobs) if jobs.is_empty() => {
186 info!("No jobs found");
187 std::thread::sleep(Duration::from_secs(APP_CONFIG.load_duration));
188 }
189 Ok(jobs) => {
190 info!(
191 event = "jobs_fetched",
192 job_count = jobs.len(),
193 "Fetched package scan jobs"
194 );
195
196 if jobs.iter().any(|job| job.hash != client.rules_state.hash) {
197 info!(
198 "At least one job requires rules other than {}, updating rules",
199 client.rules_state.hash
200 );
201
202 if let Err(err) = client.update_rules() {
203 error!("Error while updating rules: {err}");
204 }
205 }
206
207 run_jobs(&client, jobs, APP_CONFIG.threads);
208 }
209
210 Err(err) => {
211 error!("Unexpected HTTP error: {err}");
212 std::thread::sleep(Duration::from_secs(APP_CONFIG.load_duration));
213 }
214 }
215 }
216}