assemblyline_models/
config.rs

1use std::{collections::HashMap, path::PathBuf};
2
3use serde::{Deserialize, Serialize};
4use serde_with::{SerializeDisplay, DeserializeFromStr};
5
6use crate::types::ServiceName;
7
8
9/// Named Value
10#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Hash, Clone)]
11pub struct NamedValue {
12    /// Name
13    pub name: String,
14    /// Value
15    pub value: String
16}
17
18/// Webhook Configuration
19#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Hash, Clone)]
20pub struct Webhook {
21    /// Password used to authenticate with source
22    #[serde(default)]
23    pub password: Option<String>,
24    /// CA cert for source
25    #[serde(default)]
26    pub ca_cert: Option<String>,
27    /// Ignore SSL errors when reaching out to source?
28    #[serde(default)]
29    pub ssl_ignore_errors: bool,
30    #[serde(default)]
31    pub ssl_ignore_hostname: bool,
32    /// Proxy server for source
33    #[serde(default)]
34    pub proxy: Option<String>,
35    /// HTTP method used to access webhook
36    #[serde(default="default_webhook_method")]
37    pub method: String,
38    /// URI to source
39    pub uri: String,
40    /// Username used to authenticate with source
41    #[serde(default)]
42    pub username: Option<String>,
43    /// Headers
44    #[serde(default)]
45    pub headers: Vec<NamedValue>,
46    /// Number of attempts to connect to webhook endpoint
47    #[serde(default="default_webhook_retries")]
48    pub retries: Option<u32>,
49}
50
51fn default_webhook_method() -> String { "POST".to_string() }
52fn default_webhook_retries() -> Option<u32> { Some(3) }
53
54/// Resubmission Options
55#[derive(Debug, Default, Serialize, Deserialize)]
56#[serde(default)]
57pub struct ResubmitOptions {
58    pub additional_services: Vec<ServiceName>,
59    pub random_below: Option<i32>,
60}
61
62/// Postprocessing Action
63#[derive(Debug, Serialize, Deserialize)]
64pub struct PostprocessAction {
65    /// Is this action active
66    #[serde(default)]
67    pub enabled: bool,
68    /// Should this action run on cache hits
69    #[serde(default)]
70    pub run_on_cache: bool,
71    /// Should this action run on newly completed submissions
72    #[serde(default)]
73    pub run_on_completed: bool,
74    /// Query string to select submissions
75    pub filter: String,
76    /// Webhook action configuration
77    #[serde(default)]
78    pub webhook: Option<Webhook>,
79    /// Raise an alert when this action is triggered
80    #[serde(default)]
81    pub raise_alert: bool,
82    /// Resubmission configuration
83    #[serde(default)]
84    pub resubmit: Option<ResubmitOptions>,
85    /// Archive the submission when this action is triggered
86    #[serde(default)]
87    pub archive_submission: bool,
88}
89
90impl PostprocessAction {
91    pub fn new(filter: String) -> Self {
92        Self {
93            enabled: Default::default(),
94            run_on_cache: Default::default(),
95            run_on_completed: Default::default(),
96            filter,
97            webhook: Default::default(),
98            raise_alert: Default::default(),
99            resubmit: Default::default(),
100            archive_submission: Default::default(),
101        }
102    }
103
104    pub fn enable(mut self) -> Self {
105        self.enabled = true; self
106    }
107
108    pub fn alert(mut self) -> Self {
109        self.raise_alert = true; self
110    }
111
112    pub fn on_completed(mut self) -> Self {
113        self.run_on_completed = true; self
114    }
115}
116
117pub fn default_postprocess_actions() -> HashMap<String, PostprocessAction> {
118    // Raise alerts for all submissions over 500, both on cache hits and submission complete
119    [("default_alerts".to_string(), PostprocessAction{
120        enabled: true,
121        run_on_cache: true,
122        run_on_completed: true,
123        filter: "max_score: >=500".to_string(),
124        webhook: None,
125        raise_alert: true,
126        resubmit: None,
127        archive_submission: false
128    }),
129    // Resubmit submissions on completion. All submissions with score >= 0 are elegable, but sampling
130    // is applied to scores below 500
131    ("default_resubmit".to_string(), PostprocessAction{
132        enabled: true,
133        run_on_cache: true,
134        run_on_completed: true,
135        filter: "max_score: >=0".to_string(),
136        webhook: None,
137        raise_alert: false,
138        resubmit: Some(ResubmitOptions{
139            additional_services: vec![],
140            random_below: Some(500)
141        }),
142        archive_submission: false
143    })].into_iter().collect()
144}
145
146// from typing import Dict, List
147
148// from assemblyline import odm
149// from assemblyline.odm.models.service import EnvironmentVariable
150// from assemblyline.odm.models.service_delta import DockerConfigDelta
151
152
153// AUTO_PROPERTY_TYPE = ['access', 'classification', 'type', 'role', 'remove_role', 'group']
154// DEFAULT_EMAIL_FIELDS = ['email', 'emails', 'extension_selectedEmailAddress', 'otherMails', 'preferred_username', 'upn']
155
156
157// @odm.model(index=False, store=False, description="Password Requirement")
158// class PasswordRequirement(odm.Model):
159//     lower: bool = odm.Boolean(description="Password must contain lowercase letters")
160//     number: bool = odm.Boolean(description="Password must contain numbers")
161//     special: bool = odm.Boolean(description="Password must contain special characters")
162//     upper: bool = odm.Boolean(description="Password must contain uppercase letters")
163//     min_length: int = odm.Integer(description="Minimum password length")
164
165
166// DEFAULT_PASSWORD_REQUIREMENTS = {
167//     "lower": False,
168//     "number": False,
169//     "special": False,
170//     "upper": False,
171//     "min_length": 12
172// }
173
174
175// @odm.model(index=False, store=False,
176//            description="Configuration block for [GC Notify](https://notification.canada.ca/) signup and password reset")
177// class Notify(odm.Model):
178//     base_url: str = odm.Optional(odm.Keyword(), description="Base URL")
179//     api_key: str = odm.Optional(odm.Keyword(), description="API key")
180//     registration_template: str = odm.Optional(odm.Keyword(), description="Registration template")
181//     password_reset_template: str = odm.Optional(odm.Keyword(), description="Password reset template")
182//     authorization_template: str = odm.Optional(odm.Keyword(), description="Authorization template")
183//     activated_template: str = odm.Optional(odm.Keyword(), description="Activated Template")
184
185
186// DEFAULT_NOTIFY = {
187//     "base_url": None,
188//     "api_key": None,
189//     "registration_template": None,
190//     "password_reset_template": None,
191//     "authorization_template": None,
192//     "activated_template": None,
193// }
194
195
196// @odm.model(index=False, store=False, description="Configuration block for SMTP signup and password reset")
197// class SMTP(odm.Model):
198//     from_adr: str = odm.Optional(odm.Keyword(), description="Email address used for sender")
199//     host: str = odm.Optional(odm.Keyword(), description="SMTP host")
200//     password: str = odm.Optional(odm.Keyword(), description="Password for SMTP server")
201//     port: int = odm.Integer(description="Port of SMTP server")
202//     tls: bool = odm.Boolean(description="Should we communicate with SMTP server via TLS?")
203//     user: str = odm.Optional(odm.Keyword(), description="User to authenticate to the SMTP server")
204
205
206// DEFAULT_SMTP = {
207//     "from_adr": None,
208//     "host": None,
209//     "password": None,
210//     "port": 587,
211//     "tls": True,
212//     "user": None
213// }
214
215
216// @odm.model(index=False, store=False, description="Signup Configuration")
217// class Signup(odm.Model):
218//     enabled: bool = odm.Boolean(description="Can a user automatically signup for the system")
219//     smtp: SMTP = odm.Compound(SMTP, default=DEFAULT_SMTP, description="Signup via SMTP")
220//     notify: Notify = odm.Compound(Notify, default=DEFAULT_NOTIFY, description="Signup via GC Notify")
221//     valid_email_patterns: List[str] = odm.List(
222//         odm.Keyword(),
223//         description="Email patterns that will be allowed to automatically signup for an account")
224
225
226// DEFAULT_SIGNUP = {
227//     "enabled": False,
228//     "notify": DEFAULT_NOTIFY,
229//     "smtp": DEFAULT_SMTP,
230//     "valid_email_patterns": [".*", ".*@localhost"]
231// }
232
233
234// @odm.model(index=False, store=False)
235// class AutoProperty(odm.Model):
236//     field: str = odm.Keyword(description="Field to apply `pattern` to")
237//     pattern: str = odm.Keyword(description="Regex pattern for auto-prop assignment")
238//     type: str = odm.Enum(AUTO_PROPERTY_TYPE, description="Type of property assignment on pattern match")
239//     value: List[str] = odm.List(odm.Keyword(), auto=True, default=[], description="Assigned property value")
240
241
242// @odm.model(index=False, store=False, description="LDAP Configuration")
243// class LDAP(odm.Model):
244//     enabled: bool = odm.Boolean(description="Should LDAP be enabled or not?")
245//     admin_dn: str = odm.Optional(odm.Keyword(), description="DN of the group or the user who will get admin privileges")
246//     bind_user: str = odm.Optional(odm.Keyword(), description="User use to query the LDAP server")
247//     bind_pass: str = odm.Optional(odm.Keyword(), description="Password used to query the LDAP server")
248//     auto_create: bool = odm.Boolean(description="Auto-create users if they are missing")
249//     auto_sync: bool = odm.Boolean(description="Should we automatically sync with LDAP server on each login?")
250//     auto_properties: List[AutoProperty] = odm.List(odm.Compound(AutoProperty), default=[],
251//                                                    description="Automatic role and classification assignments")
252//     base: str = odm.Keyword(description="Base DN for the users")
253//     classification_mappings: Dict[str, str] = odm.Any(description="Classification mapping")
254//     email_field: str = odm.Keyword(description="Name of the field containing the email address")
255//     group_lookup_query: str = odm.Keyword(description="How the group lookup is queried")
256//     image_field: str = odm.Keyword(description="Name of the field containing the user's avatar")
257//     image_format: str = odm.Keyword(description="Type of image used to store the avatar")
258//     name_field: str = odm.Keyword(description="Name of the field containing the user's name")
259//     signature_importer_dn: str = odm.Optional(
260//         odm.Keyword(),
261//         description="DN of the group or the user who will get signature_importer role")
262//     signature_manager_dn: str = odm.Optional(
263//         odm.Keyword(),
264//         description="DN of the group or the user who will get signature_manager role")
265//     uid_field: str = odm.Keyword(description="Field name for the UID")
266//     uri: str = odm.Keyword(description="URI to the LDAP server")
267
268
269// DEFAULT_LDAP = {
270//     "enabled": False,
271//     "bind_user": None,
272//     "bind_pass": None,
273//     "auto_create": True,
274//     "auto_sync": True,
275//     "auto_properties": [],
276//     "base": "ou=people,dc=assemblyline,dc=local",
277//     "email_field": "mail",
278//     "group_lookup_query": "(&(objectClass=Group)(member=%s))",
279//     "image_field": "jpegPhoto",
280//     "image_format": "jpeg",
281//     "name_field": "cn",
282//     "uid_field": "uid",
283//     "uri": "ldap://localhost:389",
284
285//     # Deprecated
286//     "admin_dn": None,
287//     "classification_mappings": {},
288//     "signature_importer_dn": None,
289//     "signature_manager_dn": None,
290// }
291
292
293// @odm.model(index=False, store=False, description="Internal Authentication Configuration")
294// class Internal(odm.Model):
295//     enabled: bool = odm.Boolean(description="Internal authentication allowed?")
296//     failure_ttl: int = odm.Integer(description="How long to wait after `max_failures` before re-attempting login?")
297//     max_failures: int = odm.Integer(description="Maximum number of fails allowed before timeout")
298//     password_requirements: PasswordRequirement = odm.Compound(PasswordRequirement,
299//                                                               default=DEFAULT_PASSWORD_REQUIREMENTS,
300//                                                               description="Password requirements")
301//     signup: Signup = odm.Compound(Signup, default=DEFAULT_SIGNUP, description="Signup method")
302
303
304// DEFAULT_INTERNAL = {
305//     "enabled": True,
306//     "failure_ttl": 60,
307//     "max_failures": 5,
308//     "password_requirements": DEFAULT_PASSWORD_REQUIREMENTS,
309//     "signup": DEFAULT_SIGNUP
310// }
311
312
313// @odm.model(index=False, store=False, description="App provider")
314// class AppProvider(odm.Model):
315//     access_token_url: str = odm.Keyword(description="URL used to get the access token")
316//     user_get: str = odm.Optional(odm.Keyword(), description="Path from the base_url to fetch the user info")
317//     group_get: str = odm.Optional(odm.Keyword(), description="Path from the base_url to fetch the group info")
318//     scope: str = odm.Keyword()
319//     client_id: str = odm.Optional(odm.Keyword(), description="ID of your application to authenticate to the OAuth")
320//     client_secret: str = odm.Optional(odm.Keyword(),
321//                                       description="Password to your application to authenticate to the OAuth provider")
322
323
324// @odm.model(index=False, store=False, description="OAuth Provider Configuration")
325// class OAuthProvider(odm.Model):
326//     auto_create: bool = odm.Boolean(default=True, description="Auto-create users if they are missing")
327//     auto_sync: bool = odm.Boolean(default=False, description="Should we automatically sync with OAuth provider?")
328//     auto_properties: List[AutoProperty] = odm.List(odm.Compound(AutoProperty), default=[],
329//                                                    description="Automatic role and classification assignments")
330//     app_provider: AppProvider = odm.Optional(odm.Compound(AppProvider))
331//     uid_randomize: bool = odm.Boolean(default=False,
332//                                       description="Should we generate a random username for the authenticated user?")
333//     uid_randomize_digits: int = odm.Integer(default=0,
334//                                             description="How many digits should we add at the end of the username?")
335//     uid_randomize_delimiter: str = odm.Keyword(default="-",
336//                                                description="What is the delimiter used by the random name generator?")
337//     uid_regex: str = odm.Optional(
338//         odm.Keyword(),
339//         description="Regex used to parse an email address and capture parts to create a user ID out of it")
340//     uid_format: str = odm.Optional(odm.Keyword(),
341//                                    description="Format of the user ID based on the captured parts from the regex")
342//     client_id: str = odm.Optional(odm.Keyword(),
343//                                   description="ID of your application to authenticate to the OAuth provider")
344//     client_secret: str = odm.Optional(odm.Keyword(),
345//                                       description="Password to your application to authenticate to the OAuth provider")
346//     request_token_url: str = odm.Optional(odm.Keyword(), description="URL to request token")
347//     request_token_params: str = odm.Optional(odm.Keyword(), description="Parameters to request token")
348//     access_token_url: str = odm.Optional(odm.Keyword(), description="URL to get access token")
349//     access_token_params: str = odm.Optional(odm.Keyword(), description="Parameters to get access token")
350//     authorize_url: str = odm.Optional(odm.Keyword(), description="URL used to authorize access to a resource")
351//     authorize_params: str = odm.Optional(odm.Keyword(), description="Parameters used to authorize access to a resource")
352//     api_base_url: str = odm.Optional(odm.Keyword(), description="Base URL for downloading the user's and groups info")
353//     client_kwargs: Dict[str, str] = odm.Optional(odm.Mapping(odm.Keyword()),
354//                                                  description="Keyword arguments passed to the different URLs")
355//     jwks_uri: str = odm.Optional(odm.Keyword(), description="URL used to verify if a returned JWKS token is valid")
356//     uid_field: str = odm.Optional(odm.Keyword(), description="Name of the field that will contain the user ID")
357//     user_get: str = odm.Optional(odm.Keyword(), description="Path from the base_url to fetch the user info")
358//     user_groups: str = odm.Optional(odm.Keyword(), description="Path from the base_url to fetch the group info")
359//     user_groups_data_field: str = odm.Optional(
360//         odm.Keyword(),
361//         description="Field return by the group info API call that contains the list of groups")
362//     user_groups_name_field: str = odm.Optional(
363//         odm.Keyword(),
364//         description="Name of the field in the list of groups that contains the name of the group")
365//     use_new_callback_format: bool = odm.Boolean(default=False, description="Should we use the new callback method?")
366//     allow_external_tokens: bool = odm.Boolean(
367//         default=False, description="Should token provided to the login API directly be use for authentication?")
368//     external_token_alternate_audiences: List[str] = odm.List(
369//         odm.Keyword(), default=[], description="List of valid alternate audiences for the external token.")
370//     email_fields: List[str] = odm.List(odm.Keyword(), default=DEFAULT_EMAIL_FIELDS,
371//                                        description="List of fields in the claim to get the email from")
372//     username_field: str = odm.Keyword(default='uname', description="Name of the field that will contain the username")
373
374
375// DEFAULT_OAUTH_PROVIDER_AZURE = {
376//     "access_token_url": 'https://login.microsoftonline.com/common/oauth2/token',
377//     "api_base_url": 'https://login.microsoft.com/common/',
378//     "authorize_url": 'https://login.microsoftonline.com/common/oauth2/authorize',
379//     "client_id": None,
380//     "client_secret": None,
381//     "client_kwargs": {"scope": "openid email profile"},
382//     "jwks_uri": "https://login.microsoftonline.com/common/discovery/v2.0/keys",
383//     "user_get": "openid/userinfo"
384// }
385
386// DEFAULT_OAUTH_PROVIDER_GOOGLE = {
387//     "access_token_url": 'https://oauth2.googleapis.com/token',
388//     "api_base_url": 'https://openidconnect.googleapis.com/',
389//     "authorize_url": 'https://accounts.google.com/o/oauth2/v2/auth',
390//     "client_id": None,
391//     "client_secret": None,
392//     "client_kwargs": {"scope": "openid email profile"},
393//     "jwks_uri": "https://www.googleapis.com/oauth2/v3/certs",
394//     "user_get": "v1/userinfo"
395// }
396
397// DEFAULT_OAUTH_PROVIDER_AUTH_ZERO = {
398//     "access_token_url": 'https://{TENANT}.auth0.com/oauth/token',
399//     "api_base_url": 'https://{TENANT}.auth0.com/',
400//     "authorize_url": 'https://{TENANT}.auth0.com/authorize',
401//     "client_id": None,
402//     "client_secret": None,
403//     "client_kwargs": {"scope": "openid email profile"},
404//     "jwks_uri": "https://{TENANT}.auth0.com/.well-known/jwks.json",
405//     "user_get": "userinfo"
406// }
407
408// DEFAULT_OAUTH_PROVIDERS = {
409//     'auth0': DEFAULT_OAUTH_PROVIDER_AUTH_ZERO,
410//     'azure_ad': DEFAULT_OAUTH_PROVIDER_AZURE,
411//     'google': DEFAULT_OAUTH_PROVIDER_GOOGLE,
412// }
413
414
415// @odm.model(index=False, store=False, description="OAuth Configuration")
416// class OAuth(odm.Model):
417//     enabled: bool = odm.Boolean(description="Enable use of OAuth?")
418//     gravatar_enabled: bool = odm.Boolean(description="Enable gravatar?")
419//     providers: Dict[str, OAuthProvider] = odm.Mapping(odm.Compound(OAuthProvider), default=DEFAULT_OAUTH_PROVIDERS,
420//                                                       description="OAuth provider configuration")
421
422
423// DEFAULT_OAUTH = {
424//     "enabled": False,
425//     "gravatar_enabled": True,
426//     "providers": DEFAULT_OAUTH_PROVIDERS
427// }
428
429
430// @odm.model(index=False, store=False, description="Authentication Methods")
431// class Auth(odm.Model):
432//     allow_2fa: bool = odm.Boolean(description="Allow 2FA?")
433//     allow_apikeys: bool = odm.Boolean(description="Allow API keys?")
434//     allow_extended_apikeys: bool = odm.Boolean(description="Allow extended API keys?")
435//     allow_security_tokens: bool = odm.Boolean(description="Allow security tokens?")
436//     internal: Internal = odm.Compound(Internal, default=DEFAULT_INTERNAL,
437//                                       description="Internal authentication settings")
438//     ldap: LDAP = odm.Compound(LDAP, default=DEFAULT_LDAP, description="LDAP settings")
439//     oauth: OAuth = odm.Compound(OAuth, default=DEFAULT_OAUTH, description="OAuth settings")
440
441
442// DEFAULT_AUTH = {
443//     "allow_2fa": True,
444//     "allow_apikeys": True,
445//     "allow_extended_apikeys": True,
446//     "allow_security_tokens": True,
447//     "internal": DEFAULT_INTERNAL,
448//     "ldap": DEFAULT_LDAP,
449//     "oauth": DEFAULT_OAUTH
450// }
451
452
453// @odm.model(index=False, store=False, description="Alerter Configuration")
454// class Alerter(odm.Model):
455//     alert_ttl: int = odm.Integer(description="Time to live (days) for an alert in the system")
456//     constant_alert_fields: List[str] = odm.List(
457//         odm.Keyword(), description="List of fields that should not change during an alert update")
458//     default_group_field: str = odm.Keyword(description="Default field used for alert grouping view")
459//     delay: int = odm.Integer(
460//         description="Time in seconds that we give extended scans and workflow to complete their work "
461//                     "before we start showing alerts in the alert viewer.")
462//     filtering_group_fields: List[str] = odm.List(
463//         odm.Keyword(),
464//         description="List of group fields that when selected will ignore certain alerts where this field is missing.")
465//     non_filtering_group_fields: List[str] = odm.List(
466//         odm.Keyword(), description="List of group fields that are sure to be present in all alerts.")
467//     process_alert_message: str = odm.Keyword(
468//         description="Python path to the function that will process an alert message.")
469//     threshold: int = odm.Integer(description="Minimum score to reach for a submission to be considered an alert.")
470
471
472// DEFAULT_ALERTER = {
473//     "alert_ttl": 90,
474//     "constant_alert_fields": ["alert_id", "file", "ts"],
475//     "default_group_field": "file.sha256",
476//     "delay": 300,
477//     "filtering_group_fields": [
478//         "file.name",
479//         "status",
480//         "priority"
481//     ],
482//     "non_filtering_group_fields": [
483//         "file.md5",
484//         "file.sha1",
485//         "file.sha256"
486//     ],
487//     "process_alert_message": "assemblyline_core.alerter.processing.process_alert_message",
488//     "threshold": 500
489// }
490
491#[derive(Serialize, Deserialize)]
492#[serde(default)]
493pub struct Classification {
494    pub path: Option<PathBuf>,
495    pub config: Option<String>,
496}
497
498impl Default for Classification {
499    fn default() -> Self {
500        Self { 
501            path: Some("/etc/assemblyline/classification.yml".into()), 
502            config: None,
503        }
504    }
505}
506
507
508/// Dispatcher Configuration
509#[derive(Serialize, Deserialize)]
510#[serde(default)]
511pub struct Dispatcher {
512    /// Time between re-dispatching attempts, as long as some action (submission or any task completion) happens before this timeout ends, the timeout resets.
513    pub timeout: f64,
514    /// Maximum submissions allowed to be in-flight
515    pub max_inflight: u64,
516}
517
518impl Default for Dispatcher {
519    fn default() -> Self {
520        Self { 
521            timeout: 15.0 * 60.0, 
522            max_inflight: 1000 
523        }
524    }
525}
526
527
528// Configuration options regarding data expiry
529#[derive(Serialize, Deserialize)]
530#[serde(default)]
531pub struct Expiry {
532    /// Perform expiry in batches?<br>Delete queries are rounded by day therefore all delete operation happen at the same time at midnight
533    pub batch_delete: bool,
534    /// Delay, in hours, that will be applied to the expiry query so we can keep data longer then previously set or we can offset deletion during non busy hours
535    pub delay: u32,
536    /// Should we also cleanup the file storage?
537    pub delete_storage: bool,
538    /// Time, in seconds, to sleep in between each expiry run
539    pub sleep_time: u32,
540    /// Number of concurrent workers
541    pub workers: u32,
542    /// Worker processes for file storage deletes.
543    pub delete_workers: u32,
544    /// How many query chunks get run per iteration.
545    pub iteration_max_tasks: u32,
546    /// How large a batch get deleted per iteration.
547    pub delete_batch_size: u32,
548    /// The default period, in days, before tags expire from Badlist
549    pub badlisted_tag_dtl: u32,
550}
551
552impl Default for Expiry {
553    fn default() -> Self {
554        Self { 
555            batch_delete: false, 
556            delay: 0, 
557            delete_storage: true, 
558            sleep_time: 15, 
559            workers: 20, 
560            delete_workers: 2, 
561            iteration_max_tasks: 20, 
562            delete_batch_size: 200, 
563            badlisted_tag_dtl: 0 
564        }
565    }
566}
567
568
569#[derive(strum::EnumIter, strum::Display, strum::EnumString, SerializeDisplay, DeserializeFromStr, PartialEq, Eq, Hash)]
570#[strum(ascii_case_insensitive, serialize_all = "kebab-case")]
571pub enum Priority {
572    Low,
573    Medium,
574    High,
575    Critical,
576    UserLow,
577    UserMedium,
578    UserHigh,
579}
580
581impl Priority {
582    pub fn range(&self) -> (u16, u16) {
583        match self {
584            Priority::Low => (0, 100),
585            Priority::Medium => (101, 200),
586            Priority::High => (201, 300),
587            Priority::Critical => (301, 400),
588            Priority::UserLow => (401, 500),
589            Priority::UserMedium => (501, 1000),
590            Priority::UserHigh => (1001, 1500),
591        }
592    }
593}
594
595
596/// Ingester Configuration
597#[derive(Serialize, Deserialize)]
598#[serde(default)]
599pub struct Ingester {
600    // /// Default user for bulk ingestion and unattended submissions
601    // pub default_user: str = odm.Keyword()
602    // /// Default service selection
603    // pub default_services: List[str] = odm.List(odm.Keyword(), )
604    // /// Default service selection for resubmits
605    // pub default_resubmit_services: List[str] = odm.List(odm.Keyword(), )
606    // /// A prefix for descriptions. When a description is automatically generated, it will be the hash prefixed by this string
607    // pub description_prefix: str = odm.Keyword()
608    // /// Path to a callback function filtering ingestion tasks that should have their priority forcefully reset to low
609    // pub is_low_priority: str = odm.Keyword()
610    // get_whitelist_verdict: str = odm.Keyword()
611    // whitelist: str = odm.Keyword()
612    // /// How many extracted files may be added to a Submission. Overrideable via submission parameters.
613    // pub default_max_extracted: int = odm.Integer()
614    // /// How many supplementary files may be added to a Submission. Overrideable via submission parameters
615    // pub default_max_supplementary: int = odm.Integer()
616    /// Period, in seconds, in which a task should be expired
617    pub expire_after: f32,
618    /// Drop a task altogether after this many seconds
619    pub stale_after_seconds: f32,
620    /// How long should scores be kept before expiry
621    pub incomplete_expire_after_seconds: f32,
622    /// How long should scores be cached in the ingester
623    pub incomplete_stale_after_seconds: f32,
624    /// Thresholds at certain buckets before sampling
625    pub sampling_at: HashMap<Priority, i64>,
626    /// How many files to send to dispatcher concurrently
627    pub max_inflight: u64,
628    /// How long are files results cached
629    pub cache_dtl: u32,
630    /// Always create submissions even on cache hit?
631    pub always_create_submission: bool,
632}
633
634impl Default for Ingester {
635    fn default() -> Self {
636        Self {
637            cache_dtl: 2,
638//     'default_user': 'internal',
639//     'default_services': [],
640//     'default_resubmit_services': [],
641//     'description_prefix': 'Bulk',
642//     'is_low_priority': 'assemblyline.common.null.always_false',
643//     'get_whitelist_verdict': 'assemblyline.common.signaturing.drop',
644//     'whitelist': 'assemblyline.common.null.whitelist',
645//     'default_max_extracted': 100,
646//     'default_max_supplementary': 100,
647            expire_after: 15.0 * 24.0 * 60.0 * 60.0,
648            stale_after_seconds: 1.0 * 24.0 * 60.0 * 60.0,
649            incomplete_expire_after_seconds: 3600.0,
650            incomplete_stale_after_seconds: 1800.0,
651            sampling_at: [
652                (Priority::Low, 10000000),
653                (Priority::Medium, 2000000),
654                (Priority::High, 1000000),
655                (Priority::Critical, 500000),
656            ].into_iter().collect(),
657            max_inflight: 5000,
658            always_create_submission: false,
659        }
660    }
661}
662
663
664/// Redis Service configuration
665#[derive(Serialize, Deserialize)]
666pub struct RedisServer {
667    /// Hostname of Redis instance
668    pub host: String,
669    /// Port of Redis instance
670    pub port: u16,
671    /// Which db to connect to
672    #[serde(default)]
673    pub db: i64,
674}
675
676fn default_redis_nonpersistant() -> RedisServer {
677    RedisServer {
678        host: "127.0.0.1".to_owned(),
679        port: 6379,
680        db: 0,
681    }
682}
683
684fn default_redis_persistant() -> RedisServer {
685    RedisServer {
686        host: "127.0.0.1".to_owned(),
687        port: 6380,
688        db: 0,
689    }
690}
691
692
693// @odm.model(index=False, store=False)
694// class ESMetrics(odm.Model):
695//     hosts: List[str] = odm.Optional(odm.List(odm.Keyword()), description="Elasticsearch hosts")
696//     host_certificates: str = odm.Optional(odm.Keyword(), description="Host certificates")
697//     warm = odm.Integer(description="How long, per unit of time, should a document remain in the 'warm' tier?")
698//     cold = odm.Integer(description="How long, per unit of time, should a document remain in the 'cold' tier?")
699//     delete = odm.Integer(description="How long, per unit of time, should a document remain before being deleted?")
700//     unit = odm.Enum(['d', 'h', 'm'], description="Unit of time used by `warm`, `cold`, `delete` phases")
701
702
703// DEFAULT_ES_METRICS = {
704//     'hosts': None,
705//     'host_certificates': None,
706//     'warm': 2,
707//     'cold': 30,
708//     'delete': 90,
709//     'unit': 'd'
710// }
711
712
713#[derive(Serialize, Deserialize, Default)]
714#[serde(default)]
715pub struct APMServer {
716    /// URL to API server
717    pub server_url: Option<String>,
718    /// Authentication token for server
719    pub token: Option<String>,
720}
721
722
723/// Metrics Configuration
724#[derive(Serialize, Deserialize)]
725#[serde(default)]
726pub struct Metrics {
727    /// APM server configuration
728    pub apm_server: APMServer,
729//     elasticsearch: ESMetrics = odm.Compound(ESMetrics, default=DEFAULT_ES_METRICS, description="Where to export metrics?")
730    /// How often should we be exporting metrics in seconds?
731    pub export_interval: u32,
732    /// Redis for Dashboard metrics
733    pub redis: RedisServer,
734}
735
736impl Default for Metrics {
737    fn default() -> Self {
738        Self { 
739            apm_server: Default::default(),
740            export_interval: 5, 
741            redis: default_redis_nonpersistant()
742        }
743    }
744}
745
746
747#[derive(Serialize, Deserialize, Default)]
748/// Malware Archive Configuration
749#[serde(default)]
750pub struct Archiver {
751    /// List of minimum required service before archiving takes place
752    pub minimum_required_services: Vec<ServiceName>,
753}
754
755/// Redis Configuration
756#[derive(Serialize, Deserialize)]
757#[serde(default)]
758pub struct Redis {
759    /// A volatile Redis instance
760    pub nonpersistent: RedisServer,
761    /// A persistent Redis instance
762    pub persistent: RedisServer,
763}
764
765impl Default for Redis {
766    fn default() -> Self {
767        Self { 
768            nonpersistent: default_redis_nonpersistant(), 
769            persistent: default_redis_persistant()
770        }
771    }
772}
773
774
775// @odm.model(index=False, store=False, description="A configuration for mounting existing volumes to a container")
776// class Mount(odm.Model):
777//     name: str = odm.Keyword(description="Name of volume mount")
778//     path: str = odm.Text(description="Target mount path")
779//     read_only: bool = odm.Boolean(default=True, description="Should this be mounted as read-only?")
780//     privileged_only: bool = odm.Boolean(default=False,
781//                                         description="Should this mount only be available for privileged services?")
782
783//     # Kubernetes-specific
784//     resource_type: str = odm.Enum(default='volume', values=['secret', 'configmap', 'volume'],
785//                                   description="Type of mountable Kubernetes resource")
786//     resource_name: str = odm.Optional(odm.Keyword(), description="Name of resource (Kubernetes only)")
787//     resource_key: str = odm.Optional(odm.Keyword(), description="Key of ConfigMap/Secret (Kubernetes only)")
788
789//     # TODO: Deprecate in next major change in favour of general configuration above for mounting Kubernetes resources
790//     config_map: str = odm.Optional(odm.Keyword(), description="Name of ConfigMap (Kubernetes only, deprecated)")
791//     key: str = odm.Optional(odm.Keyword(), description="Key of ConfigMap (Kubernetes only, deprecated)")
792
793
794// @odm.model(index=False, store=False,
795//            description="A set of default values to be used running a service when no other value is set")
796// class ScalerServiceDefaults(odm.Model):
797//     growth: int = odm.Integer(description="Period, in seconds, to wait before scaling up a service deployment")
798//     shrink: int = odm.Integer(description="Period, in seconds, to wait before scaling down a service deployment")
799//     backlog: int = odm.Integer(description="Backlog threshold that dictates scaling adjustments")
800//     min_instances: int = odm.Integer(description="The minimum number of service instances to be running")
801//     environment: List[EnvironmentVariable] = odm.List(odm.Compound(EnvironmentVariable), default=[],
802//                                                       description="Environment variables to pass onto services")
803//     mounts: List[Mount] = odm.List(odm.Compound(Mount), default=[],
804//                                    description="A list of volume mounts for every service")
805
806
807// # The operations we support for label and field selectors are based on the common subset of
808// # what kubernetes supports on the list_node API endpoint and the nodeAffinity field
809// # on pod specifications. The selector needs to work in both cases because we use these
810// # selectors both for probing what nodes are available (list_node) and making sure
811// # the pods only run on the pods that are returned there (using nodeAffinity)
812
813// @odm.model(index=False, store=False, description="Limit a set of kubernetes objects based on a field query.")
814// class FieldSelector(odm.Model):
815//     key = odm.keyword(description="Name of a field to select on.")
816//     equal = odm.boolean(default=True, description="When true key must equal value, when false it must not")
817//     value = odm.keyword(description="Value to compare field to.")
818
819
820// # Excluded from this list is Gt and Lt for above reason
821// KUBERNETES_LABEL_OPS = ['In', 'NotIn', 'Exists', 'DoesNotExist']
822
823
824// @odm.model(index=False, store=False, description="Limit a set of kubernetes objects based on a label query.")
825// class LabelSelector(odm.Model):
826//     key = odm.keyword(description="Name of label to select on.")
827//     operator = odm.Enum(KUBERNETES_LABEL_OPS, description="Operation to select label with.")
828//     values = odm.sequence(odm.keyword(), description="Value list to compare label to.")
829
830
831// @odm.model(index=False, store=False)
832// class Selector(odm.Model):
833//     field = odm.sequence(odm.compound(FieldSelector), default=[],
834//                          description="Field selector for resource under kubernetes")
835//     label = odm.sequence(odm.compound(LabelSelector), default=[],
836//                          description="Label selector for resource under kubernetes")
837
838
839// @odm.model(index=False, store=False)
840// class Scaler(odm.Model):
841//     service_defaults: ScalerServiceDefaults = odm.Compound(ScalerServiceDefaults,
842//                                                            description="Defaults Scaler will assign to a service.")
843//     cpu_overallocation: float = odm.Float(description="Percentage of CPU overallocation")
844//     memory_overallocation: float = odm.Float(description="Percentage of RAM overallocation")
845//     overallocation_node_limit = odm.Optional(odm.Integer(description="If the system has this many nodes or "
846//                                                                      "more overallocation is ignored"))
847//     additional_labels: List[str] = odm.Optional(
848//         odm.List(odm.Text()), description="Additional labels to be applied to services('=' delimited)")
849//     linux_node_selector = odm.compound(Selector, description="Selector for linux nodes under kubernetes")
850//     # windows_node_selector = odm.compound(Selector, description="Selector for windows nodes under kubernetes")
851
852
853// DEFAULT_SCALER = {
854//     'additional_labels': None,
855//     'cpu_overallocation': 1,
856//     'memory_overallocation': 1,
857//     'overallocation_node_limit': None,
858//     'service_defaults': {
859//         'growth': 60,
860//         'shrink': 30,
861//         'backlog': 100,
862//         'min_instances': 0,
863//         'environment': [
864//             {'name': 'SERVICE_API_HOST', 'value': 'http://service-server:5003'},
865//             {'name': 'AL_SERVICE_TASK_LIMIT', 'value': 'inf'},
866//         ],
867//     },
868//     'linux_node_selector': {
869//         'field': [],
870//         'label': [],
871//     },
872//     # 'windows_node_selector': {
873//     #     'field': [],
874//     #     'label': [],
875//     # },
876// }
877
878
879// @odm.model(index=False, store=False)
880// class RegistryConfiguration(odm.Model):
881//     name: str = odm.Text(description="Name of container registry")
882//     proxies: Dict = odm.Optional(odm.Mapping(odm.Text()),
883//                                  description="Proxy configuration that is passed to Python Requests")
884
885
886// @odm.model(index=False, store=False)
887// class Updater(odm.Model):
888//     job_dockerconfig: DockerConfigDelta = odm.Compound(
889//         DockerConfigDelta, description="Container configuration used for service registration/updates")
890//     registry_configs: List = odm.List(odm.Compound(RegistryConfiguration),
891//                                       description="Configurations to be used with container registries")
892
893
894// DEFAULT_UPDATER = {
895//     'job_dockerconfig': {
896//         'cpu_cores': 1,
897//         'ram_mb': 1024,
898//         'ram_mb_min': 256,
899//     },
900//     'registry_configs': [{
901//         'name': 'registry.hub.docker.com',
902//         'proxies': {}
903//     }]
904// }
905
906
907// @odm.model(index=False, store=False)
908// class VacuumSafelistItem(odm.Model):
909//     name = odm.Keyword()
910//     conditions = odm.Mapping(odm.Keyword())
911
912
913// @odm.model(index=False, store=False)
914// class Vacuum(odm.Model):
915//     list_cache_directory: str = odm.Keyword()
916//     worker_cache_directory: str = odm.Keyword()
917//     data_directories: List[str] = odm.List(odm.Keyword())
918//     file_directories: List[str] = odm.List(odm.Keyword())
919//     assemblyline_user: str = odm.Keyword()
920//     department_map_url = odm.Optional(odm.Keyword())
921//     department_map_init = odm.Optional(odm.Keyword())
922//     stream_map_url = odm.Optional(odm.Keyword())
923//     stream_map_init = odm.Optional(odm.Keyword())
924//     safelist = odm.List(odm.Compound(VacuumSafelistItem))
925//     worker_threads: int = odm.Integer()
926//     worker_rollover: int = odm.Integer()
927//     minimum_classification: str = odm.Keyword()
928//     ingest_type = odm.keyword()
929
930
931// DEFAULT_VACUUM = dict(
932//     list_cache_directory="/cache/",
933//     worker_cache_directory="/memory/",
934//     data_directories=[],
935//     file_directories=[],
936//     assemblyline_user="vacuum-service-account",
937//     department_map_url=None,
938//     department_map_init=None,
939//     stream_map_url=None,
940//     stream_map_init=None,
941//     safelist=[],
942//     worker_threads=50,
943//     worker_rollover=1000,
944//     minimum_classification='U',
945//     ingest_type='VACUUM',
946// )
947
948
949/// Core Component Configuration
950#[derive(Serialize, Deserialize, Default)]
951#[serde(default)]
952// @odm.model(index=False, store=False, description="")
953pub struct Core {
954    // /// Configuration for Alerter
955    // #[serde(default)]
956    // pub alerter: Alerter,
957    /// Configuration for the permanent submission archive
958    pub archiver: Archiver,
959    /// Configuration for Dispatcher
960    pub dispatcher: Dispatcher,
961    /// Configuration for Expiry
962    pub expiry: Expiry,
963    /// Configuration for Ingester
964    pub ingester: Ingester,
965    /// Configuration for Metrics Collection
966    pub metrics: Metrics,
967    /// Configuration for system cleanup
968    pub plumber: Plumber,
969    /// Configuration for Redis instances
970    pub redis: Redis,
971    // /// Configuration for Scaler
972    // #[serde(default)]
973    // pub scaler: Scaler,
974    // /// Configuration for Updater
975    // #[serde(default)]
976    // pub updater: Updater,
977    // /// Configuration for Vacuum
978    // #[serde(default)]
979    // pub vacuum: Vacuum,
980}
981
982// DEFAULT_CORE = {
983//     "alerter": DEFAULT_ALERTER,
984//     "archiver": DEFAULT_ARCHIVER,
985//     "dispatcher": DEFAULT_DISPATCHER,
986//     "expiry": DEFAULT_EXPIRY,
987//     "ingester": DEFAULT_INGESTER,
988//     "metrics": DEFAULT_METRICS,
989//     "redis": DEFAULT_REDIS,
990//     "scaler": DEFAULT_SCALER,
991//     "updater": DEFAULT_UPDATER,
992// }
993
994/// Plumber Configuration
995#[derive(Serialize, Deserialize)]
996#[serde(default)]
997pub struct Plumber {
998    /// Interval in seconds at which the notification queue cleanup should run
999    pub notification_queue_interval: u64,
1000    /// Max age in seconds notification queue messages can be
1001    pub notification_queue_max_age: u64,
1002}
1003
1004impl Default for Plumber {
1005    fn default() -> Self {
1006        Self { 
1007            notification_queue_interval: 30 * 60,
1008            notification_queue_max_age: 24 * 60 * 60
1009        }
1010    }
1011}
1012
1013
1014
1015
1016/// Datastore Archive feature configuration
1017#[derive(Serialize, Deserialize)]
1018#[serde(default)]
1019pub struct Archive {
1020    /// Are we enabling Achiving features across indices?
1021    pub enabled: bool,
1022    /// List of indices the ILM Applies to
1023    pub indices: Vec<String>,
1024}
1025
1026impl Default for Archive {
1027    fn default() -> Self {
1028        Self { 
1029            enabled: false, 
1030            indices: vec!["file".to_owned(), "submission".to_owned(), "result".to_owned()], 
1031        }
1032    }
1033}
1034
1035
1036#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy)]
1037#[serde(rename_all="lowercase")]
1038pub enum DatastoreType {
1039    Elasticsearch
1040}
1041
1042#[test]
1043fn test_datastore_type_serialization() {
1044    assert_eq!(serde_json::to_string(&DatastoreType::Elasticsearch).unwrap(), "\"elasticsearch\"");
1045    assert_eq!(serde_json::from_str::<DatastoreType>("\"elasticsearch\"").unwrap(), DatastoreType::Elasticsearch);
1046    assert_eq!(serde_json::to_value(DatastoreType::Elasticsearch).unwrap(), serde_json::json!("elasticsearch"));
1047    // assert_eq!(serde_json::from_str::<DatastoreType>("\"Elasticsearch\"").unwrap(), DatastoreType::Elasticsearch);
1048
1049    #[derive(Debug, Serialize, Deserialize)]
1050    struct Test {
1051        ds: DatastoreType
1052    }
1053    let sample = Test {ds: DatastoreType::Elasticsearch};
1054    assert_eq!(serde_json::to_string(&sample).unwrap(), "{\"ds\":\"elasticsearch\"}");
1055}
1056
1057
1058/// Datastore Configuration
1059#[derive(Serialize, Deserialize)]
1060#[serde(default)]
1061pub struct Datastore {
1062    /// List of hosts used for the datastore
1063    pub hosts: Vec<String>,
1064    /// Datastore Archive feature configuration
1065    pub archive: Archive,
1066    /// Default cache lenght for computed indices (submission_tree, submission_summary...
1067    pub cache_dtl: u32,
1068    /// Type of application used for the datastore
1069    #[serde(rename="type")]
1070    pub dtype: DatastoreType,
1071}
1072
1073impl Default for Datastore {
1074    fn default() -> Self {
1075        Self {
1076            hosts: vec!["http://elastic:devpass@localhost:9200".to_owned()],
1077            archive: Default::default(),
1078            cache_dtl: 5,
1079            dtype: DatastoreType::Elasticsearch,
1080        }
1081    }
1082}
1083
1084
1085// @odm.model(index=False, store=False, description="Datasource Configuration")
1086// class Datasource(odm.Model):
1087//     classpath: str = odm.Keyword()
1088//     config: Dict[str, str] = odm.Mapping(odm.Keyword())
1089
1090
1091// DEFAULT_DATASOURCES = {
1092//     "al": {
1093//         "classpath": 'assemblyline.datasource.al.AL',
1094//         "config": {}
1095//     },
1096//     "alert": {
1097//         "classpath": 'assemblyline.datasource.alert.Alert',
1098//         "config": {}
1099//     }
1100// }
1101
1102
1103/// Filestore Configuration
1104#[derive(Serialize, Deserialize)]
1105#[serde(default)]
1106pub struct Filestore {
1107    /// List of filestores used for malware archive
1108    pub archive: Vec<String>,
1109    /// List of filestores used for caching
1110    pub cache: Vec<String>,
1111    /// List of filestores used for storage
1112    pub storage: Vec<String>,
1113}
1114
1115impl Default for Filestore {
1116    fn default() -> Self {
1117        Self { 
1118            archive: vec!["s3://al_storage_key:Ch@ngeTh!sPa33w0rd@localhost:9000?s3_bucket=al-archive&use_ssl=False".to_string()], 
1119            cache: vec!["s3://al_storage_key:Ch@ngeTh!sPa33w0rd@localhost:9000?s3_bucket=al-cache&use_ssl=False".to_string()], 
1120            storage: vec!["s3://al_storage_key:Ch@ngeTh!sPa33w0rd@localhost:9000?s3_bucket=al-storage&use_ssl=False".to_string()] 
1121        }
1122    }
1123}
1124
1125#[derive(Debug, strum::Display, SerializeDisplay, strum::EnumString, DeserializeFromStr)]
1126#[strum(serialize_all="UPPERCASE", ascii_case_insensitive)]
1127pub enum LogLevel {
1128    Debug, 
1129    Info,
1130    Warning,
1131    Error,
1132    Critical,
1133    Disabled,
1134}
1135
1136#[derive(Debug, Serialize, Deserialize)]
1137pub enum SyslogTransport {
1138    Udp,
1139    Tcp
1140}
1141
1142/// Model Definition for the Logging Configuration
1143#[derive(Debug, Serialize, Deserialize)]
1144#[serde(default)]
1145pub struct Logging {
1146    /// What level of logging should we have?
1147    pub log_level: LogLevel,
1148    /// Should we log to console?
1149    pub log_to_console: bool,
1150    /// Should we log to files on the server?
1151    pub log_to_file: bool,
1152    /// If `log_to_file: true`, what is the directory to store logs?
1153    pub log_directory: PathBuf,
1154    /// Should logs be sent to a syslog server?
1155    pub log_to_syslog: bool,
1156    /// If `log_to_syslog: true`, provide hostname/IP of the syslog server?
1157    pub syslog_host: String,
1158    /// If `log_to_syslog: true`, provide port of the syslog server?
1159    pub syslog_port: u16,
1160    /// If `log_to_syslog: true`, provide transport for syslog server?
1161    pub syslog_transport: SyslogTransport,
1162    // /// How often, in seconds, should counters log their values?
1163    // pub export_interval: int = odm.Integer(")
1164    /// Log in JSON format?
1165    pub log_as_json: bool,
1166    // /// Add a health check to core components.<br>If `true`, core components will touch this path regularly to tell the container environment it is healthy
1167    // pub heartbeat_file: str = odm.Optional(odm.Keyword(),")
1168}
1169
1170impl Default for Logging {
1171    fn default() -> Self {
1172        Self { 
1173            log_directory: "/var/log/assemblyline/".into(),
1174            log_as_json: true,
1175            log_level: LogLevel::Info,
1176            log_to_console: true,
1177            log_to_file: false,
1178            log_to_syslog: false,
1179            syslog_host: "localhost".to_owned(),
1180            syslog_port: 514,
1181            syslog_transport: SyslogTransport::Tcp,
1182            // export_interval: 5,
1183            // heartbeat_file: "/tmp/heartbeat"
1184        }
1185    }
1186}
1187
1188// SERVICE_CATEGORIES = [
1189//     'Antivirus',
1190//     'Dynamic Analysis',
1191//     'External',
1192//     'Extraction',
1193//     'Filtering',
1194//     'Internet Connected',
1195//     'Networking',
1196//     'Static Analysis',
1197// ]
1198
1199fn default_service_stages() -> Vec<String> {
1200    vec![
1201        "FILTER".to_string(),
1202        "EXTRACT".to_string(),
1203        "CORE".to_string(),
1204        "SECONDARY".to_string(),
1205        "POST".to_string(),
1206        "REVIEW".to_string(),
1207    ]
1208}
1209
1210#[derive(SerializeDisplay, DeserializeFromStr, strum::Display, strum::EnumString, Debug, Clone, Copy, PartialEq, Eq)]
1211// #[metadata_type(ElasticMeta)]
1212#[strum(serialize_all = "lowercase")]
1213pub enum SafelistHashTypes {
1214    Sha1, Sha256, Md5
1215}
1216
1217#[derive(SerializeDisplay, DeserializeFromStr, strum::Display, strum::EnumString, Debug, Clone, Copy)]
1218#[strum(serialize_all = "lowercase")]
1219pub enum RegistryTypes {
1220    Docker, 
1221    Harbor
1222}
1223
1224/// Service's Safelisting Configuration
1225// @odm.model(index=False, store=False, description="")
1226#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1227#[serde(default)]
1228pub struct ServiceSafelist {
1229    /// Should services be allowed to check extracted files against safelist?
1230    pub enabled: bool,
1231    /// Types of file hashes used for safelist checks
1232    pub hash_types: Vec<SafelistHashTypes>,
1233    /// Should the Safelist service always run on extracted files?
1234    pub enforce_safelist_service: bool,
1235}
1236
1237impl Default for ServiceSafelist {
1238    fn default() -> Self {
1239        Self { 
1240            enabled: true, 
1241            hash_types: vec![SafelistHashTypes::Sha1, SafelistHashTypes::Sha256], 
1242            enforce_safelist_service: false,
1243        }
1244    }
1245}
1246
1247// @odm.model(index=False, store=False, description="Pre-Configured Registry Details for Services")
1248// class ServiceRegistry(odm.Model):
1249//     name: str = odm.Keyword(description="Name of container registry")
1250//     type: str = odm.Enum(values=REGISTRY_TYPES, default='docker', description="Type of container registry")
1251//     username: str = odm.Keyword(description="Username for container registry")
1252//     password: str = odm.Keyword(description="Password for container registry")
1253
1254
1255/// Services Configuration
1256#[derive(Debug, Clone, Serialize, Deserialize)]
1257#[serde(default)]
1258pub struct Services {
1259//     categories: List[str] = odm.List(odm.Keyword(), description="List of categories a service can be assigned to")
1260//     default_timeout: int = odm.Integer(description="Default service timeout time in seconds")
1261    /// List of execution stages a service can be assigned to
1262    pub stages: Vec<String>, 
1263    /// Substitution variables for image paths (for custom registry support)
1264    // pub image_variables: Dict[str, str] = odm.Mapping(odm.Keyword(default=''), ),
1265    /// Similar to `image_variables` but only applied to the updater. Intended for use with local registries.
1266    // pub update_image_variables: Dict[str, str] = odm.Mapping(odm.Keyword(default=''), ),
1267    /// Default update channel to be used for new services
1268    pub preferred_update_channel: String,
1269    /// Allow fetching container images from insecure registries
1270    pub allow_insecure_registry: bool,
1271    /// Global registry type to be used for fetching updates for a service (overridable by a service)
1272    pub preferred_registry_type: RegistryTypes,
1273    /// Global preference that controls if services should be privileged to communicate with core infrastucture
1274    pub prefer_service_privileged: bool,
1275    /// How much CPU do we want to reserve relative to the service's request?<br> At `1`, a service's full CPU request will be reserved for them.<br> At `0` (only for very small appliances/dev boxes), the service's CPU will be limited ""but no CPU will be reserved allowing for more flexible scheduling of containers.
1276    pub cpu_reservation: f64,
1277    pub safelist: ServiceSafelist,
1278//     registries = odm.Optional(odm.List(odm.Compound(ServiceRegistry)), description="Global set of registries for services")
1279//     service_account = odm.optional(odm.keyword(description="Service account to use for pods in kubernetes where the service does not have one configured."))
1280}
1281
1282impl Default for Services {
1283    fn default() -> Self {
1284        Self { 
1285    //     "categories": SERVICE_CATEGORIES,
1286    //     "default_timeout": 60,
1287            stages: default_service_stages(),
1288    //     "image_variables": {},
1289    //     "update_image_variables": {},
1290            preferred_update_channel: "stable".to_string(),
1291            preferred_registry_type: RegistryTypes::Docker,
1292            prefer_service_privileged: false,
1293            allow_insecure_registry: false,
1294            cpu_reservation: 0.25,
1295            safelist: Default::default(),
1296    //     "registries": []
1297        }
1298    }
1299}
1300
1301// @odm.model(index=False, store=False, description="System Configuration")
1302// class System(odm.Model):
1303//     constants: str = odm.Keyword(description="Module path to the assemblyline constants")
1304//     organisation: str = odm.Text(description="Organisation acronym used for signatures")
1305//     type: str = odm.Enum(values=['production', 'staging', 'development'], description="Type of system")
1306
1307
1308// DEFAULT_SYSTEM = {
1309//     "constants": "assemblyline.common.constants",
1310//     "organisation": "ACME",
1311//     "type": 'production',
1312// }
1313
1314
1315// @odm.model(index=False, store=False, description="Statistics")
1316// class Statistics(odm.Model):
1317//     alert: List[str] = odm.List(odm.Keyword(),
1318//                                 description="Fields used to generate statistics in the Alerts page")
1319//     submission: List[str] = odm.List(odm.Keyword(),
1320//                                      description="Fields used to generate statistics in the Submissions page")
1321
1322
1323// DEFAULT_STATISTICS = {
1324//     "alert": [
1325//         'al.attrib',
1326//         'al.av',
1327//         'al.behavior',
1328//         'al.domain',
1329//         'al.ip',
1330//         'al.yara',
1331//         'file.name',
1332//         'file.md5',
1333//         'owner'
1334//     ],
1335//     "submission": [
1336//         'params.submitter'
1337//     ]
1338// }
1339
1340
1341// @odm.model(index=False, store=False, description="Alerting Metadata")
1342// class AlertingMeta(odm.Model):
1343//     important: List[str] = odm.List(odm.Keyword(), description="Metadata keys that are considered important")
1344//     subject: List[str] = odm.List(odm.Keyword(), description="Metadata keys that refer to an email's subject")
1345//     url: List[str] = odm.List(odm.Keyword(), description="Metadata keys that refer to a URL")
1346
1347
1348// DEFAULT_ALERTING_META = {
1349//     'important': [
1350//         'original_source',
1351//         'protocol',
1352//         'subject',
1353//         'submitted_url',
1354//         'source_url',
1355//         'url',
1356//         'web_url',
1357//         'from',
1358//         'to',
1359//         'cc',
1360//         'bcc',
1361//         'ip_src',
1362//         'ip_dst',
1363//         'source'
1364//     ],
1365//     'subject': [
1366//         'subject'
1367//     ],
1368//     'url': [
1369//         'submitted_url',
1370//         'source_url',
1371//         'url',
1372//         'web_url'
1373//     ]
1374
1375// }
1376
1377
1378// @odm.model(index=False, store=False, description="Target definition of an external link")
1379// class ExternalLinksTargets(odm.Model):
1380//     type: str = odm.Enum(values=['metadata', 'tag', 'hash'], description="Type of external link target")
1381//     key: str = odm.Keyword(description="Key that it can be used against")
1382
1383
1384// @odm.model(index=False, store=False, description="External links that specific metadata and tags can pivot to")
1385// class ExternalLinks(odm.Model):
1386//     allow_bypass: bool = odm.boolean(
1387//         default=False,
1388//         description="If the classification of the item is higher than the max_classificaiton, can we let the user "
1389//                     "bypass the check and still query the external link?")
1390//     name: str = odm.Keyword(description="Name of the link")
1391//     double_encode: bool = odm.boolean(default=False, description="Should the replaced value be double encoded?")
1392//     classification = odm.Optional(
1393//         odm.ClassificationString(description="Minimum classification the user must have to see this link"))
1394//     max_classification = odm.Optional(
1395//         odm.ClassificationString(description="Maximum classification of data that may be handled by the link"))
1396//     replace_pattern: str = odm.Keyword(
1397//         description="Pattern that will be replaced in the URL with the metadata or tag value")
1398//     targets: List[ExternalLinksTargets] = odm.List(
1399//         odm.Compound(ExternalLinksTargets),
1400//         default=[],
1401//         description="List of external sources to query")
1402//     url: str = odm.Keyword(description="URL to redirect to")
1403
1404
1405// EXAMPLE_EXTERNAL_LINK_VT = {
1406//     # This is an example on how this would work with VirusTotal
1407//     "name": "VirusTotal",
1408//     "replace_pattern": "{REPLACE}",
1409//     "targets": [
1410//         {"type": "tag", "key": "network.static.uri"},
1411//         {"type": "tag", "key": "network.dynamic.uri"},
1412//         {"type": "metadata", "key": "submitted_url"},
1413//         {"type": "hash", "key": "md5"},
1414//         {"type": "hash", "key": "sha1"},
1415//         {"type": "hash", "key": "sha256"},
1416//     ],
1417//     "url": "https://www.virustotal.com/gui/search/{REPLACE}",
1418//     "double_encode": True,
1419//     # "classification": "TLP:CLEAR",
1420//     # "max_classification": "TLP:CLEAR",
1421// }
1422
1423// EXAMPLE_EXTERNAL_LINK_MB_SHA256 = {
1424//     # This is an example on how this would work with Malware Bazaar
1425//     "name": "MalwareBazaar",
1426//     "replace_pattern": "{REPLACE}",
1427//     "targets": [
1428//         {"type": "hash", "key": "sha256"},
1429//     ],
1430//     "url": "https://bazaar.abuse.ch/sample/{REPLACE}/",
1431//     # "classification": "TLP:CLEAR",
1432//     # "max_classification": "TLP:CLEAR",
1433// }
1434
1435
1436// @odm.model(index=False, store=False, description="Connection details for external systems/data sources.")
1437// class ExternalSource(odm.Model):
1438//     name: str = odm.Keyword(description="Name of the source.")
1439//     classification = odm.Optional(
1440//         odm.ClassificationString(
1441//             description="Minimum classification applied to information from the source"
1442//                         " and required to know the existance of the source."))
1443//     max_classification = odm.Optional(
1444//         odm.ClassificationString(description="Maximum classification of data that may be handled by the source"))
1445//     url: str = odm.Keyword(description="URL of the upstream source's lookup service.")
1446
1447
1448// EXAMPLE_EXTERNAL_SOURCE_VT = {
1449//     # This is an example on how this would work with VirusTotal
1450//     "name": "VirusTotal",
1451//     "url": "vt-lookup.namespace.svc.cluster.local",
1452//     "classification": "TLP:CLEAR",
1453//     "max_classification": "TLP:CLEAR",
1454// }
1455
1456// EXAMPLE_EXTERNAL_SOURCE_MB = {
1457//     # This is an example on how this would work with Malware Bazaar
1458//     "name": "Malware Bazaar",
1459//     "url": "mb-lookup.namespace.scv.cluster.local",
1460//     "classification": "TLP:CLEAR",
1461//     "max_classification": "TLP:CLEAR",
1462// }
1463
1464
1465/// UI Configuration
1466#[derive(Serialize, Deserialize)]
1467#[serde(default)]
1468pub struct UI {
1469//     alerting_meta: AlertingMeta = odm.Compound(AlertingMeta, default=DEFAULT_ALERTING_META,description="Alerting metadata fields")
1470    /// Allow user to tell in advance the system that a file is malicious?
1471    pub allow_malicious_hinting: bool,
1472//     allow_raw_downloads: bool = odm.Boolean(description="Allow user to download raw files?")
1473//     allow_zip_downloads: bool = odm.Boolean(description="Allow user to download files as password protected ZIPs?")
1474//     allow_replay: bool = odm.Boolean(description="Allow users to request replay on another server?")
1475//     allow_url_submissions: bool = odm.Boolean(description="Allow file submissions via url?")
1476//     audit: bool = odm.Boolean(description="Should API calls be audited and saved to a separate log file?")
1477//     banner: Dict[str, str] = odm.Optional(odm.Mapping(odm.Keyword()), description="Banner message display on the main page (format: {<language_code>: message})")
1478//     banner_level: str = odm.Enum(values=["info", "warning", "success", "error"],description="Banner message level")
1479//     debug: bool = odm.Boolean(description="Enable debugging?")
1480//     discover_url: str = odm.Optional(odm.Keyword(), description="Discover URL")
1481//     download_encoding = odm.Enum(values=["raw", "cart"], description="Which encoding will be used for downloads?")
1482//     email: str = odm.Optional(odm.Email(), description="Assemblyline admins email address")
1483    /// Enforce the user's quotas?
1484    pub enforce_quota: bool,
1485//     external_links: List[ExternalLinks] = odm.List(odm.Compound(ExternalLinks),description="List of external pivot links")
1486//     external_sources: List[ExternalSource] = odm.List(odm.Compound(ExternalSource), description="List of external sources to query")
1487//     fqdn: str = odm.Text(description="Fully qualified domain name to use for the 2-factor authentication validation")
1488//     ingest_max_priority: int = odm.Integer(description="Maximum priority for ingest API")
1489//     read_only: bool = odm.Boolean(description="Turn on read only mode in the UI")
1490//     read_only_offset: str = odm.Keyword(default="", description="Offset of the read only mode for all paging and searches")
1491//     rss_feeds: List[str] = odm.List(odm.Keyword(), default=[], description="List of RSS feeds to display on the UI")
1492//     services_feed: str = odm.Keyword(description="Feed of all the services available on AL")
1493//     secret_key: str = odm.Keyword(description="Flask secret key to store cookies, etc.")
1494//     session_duration: int = odm.Integer(description="Duration of the user session before the user has to login again")
1495//     statistics: Statistics = odm.Compound(Statistics, default=DEFAULT_STATISTICS, description="Statistics configuration")
1496//     tos: str = odm.Optional(odm.Text(), description="Terms of service")
1497//     tos_lockout: bool = odm.Boolean(description="Lock out user after accepting the terms of service?")
1498//     tos_lockout_notify: List[str] = odm.Optional(odm.List(odm.Keyword()), description="List of admins to notify when a user gets locked out")
1499//     url_submission_headers: Dict[str, str] = odm.Optional(odm.Mapping(odm.Keyword()), description="Headers used by the url_download method")
1500//     url_submission_proxies: Dict[str, str] = odm.Optional(odm.Mapping(odm.Keyword()), description="Proxy used by the url_download method")
1501//     url_submission_timeout: int = odm.Integer(default=15, description="Request timeout for fetching URLs")
1502//     validate_session_ip: bool = odm.Boolean(description="Validate if the session IP matches the IP the session was created from")
1503//     validate_session_useragent: bool = odm.Boolean(description="Validate if the session useragent matches the useragent the session was created with")
1504}
1505
1506impl Default for UI {
1507    fn default() -> Self {
1508        Self { 
1509// DEFAULT_UI = {
1510//     "alerting_meta": DEFAULT_ALERTING_META,
1511            allow_malicious_hinting: false,
1512//     "allow_raw_downloads": True,
1513//     "allow_zip_downloads": True,
1514//     "allow_replay": False,
1515//     "allow_url_submissions": True,
1516//     "audit": True,
1517//     "banner": None,
1518//     "banner_level": 'info',
1519//     "debug": False,
1520//     "discover_url": None,
1521//     "download_encoding": "cart",
1522//     "email": None,
1523            enforce_quota: true,
1524//     "external_links": [],
1525//     "external_sources": [],
1526//     "fqdn": "localhost",
1527//     "ingest_max_priority": 250,
1528//     "read_only": False,
1529//     "read_only_offset": "",
1530//     "rss_feeds": [
1531//         "https://alpytest.blob.core.windows.net/pytest/stable.json",
1532//         "https://alpytest.blob.core.windows.net/pytest/services.json",
1533//         "https://alpytest.blob.core.windows.net/pytest/blog.json"
1534//     ],
1535//     "services_feed": "https://alpytest.blob.core.windows.net/pytest/services.json",
1536//     "secret_key": "This is the default flask secret key... you should change this!",
1537//     "session_duration": 3600,
1538//     "statistics": DEFAULT_STATISTICS,
1539//     "tos": None,
1540//     "tos_lockout": False,
1541//     "tos_lockout_notify": None,
1542//     "url_submission_headers": {
1543//         "User-Agent": "Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko)"
1544//                       " Chrome/110.0.0.0 Safari/537.36"
1545//     },
1546//     "url_submission_proxies": {},
1547//     "validate_session_ip": True,
1548//     "validate_session_useragent": True,
1549// }
1550        }
1551    }
1552}
1553
1554// # Options regarding all submissions, regardless of their input method
1555// @odm.model(index=False, store=False)
1556// class TagTypes(odm.Model):
1557//     attribution: List[str] = odm.List(odm.Keyword(), description="Attibution tags")
1558//     behavior: List[str] = odm.List(odm.Keyword(), description="Behaviour tags")
1559//     ioc: List[str] = odm.List(odm.Keyword(), description="IOC tags")
1560
1561
1562// DEFAULT_TAG_TYPES = {
1563//     'attribution': [
1564//         'attribution.actor',
1565//         'attribution.campaign',
1566//         'attribution.exploit',
1567//         'attribution.implant',
1568//         'attribution.family',
1569//         'attribution.network',
1570//         'av.virus_name',
1571//         'file.config',
1572//         'technique.obfuscation',
1573//     ],
1574//     'behavior': [
1575//         'file.behavior'
1576//     ],
1577//     'ioc': [
1578//         'network.email.address',
1579//         'network.static.ip',
1580//         'network.static.domain',
1581//         'network.static.uri',
1582//         'network.dynamic.ip',
1583//         'network.dynamic.domain',
1584//         'network.dynamic.uri',
1585
1586//     ]
1587// }
1588
1589
1590// @odm.model(index=False, store=False, description="A source entry for the sha256 downloader")
1591// class Sha256Source(odm.Model):
1592//     name: str = odm.Keyword(description="Name of the sha256 source")
1593//     classification = odm.Optional(
1594//         odm.ClassificationString(
1595//             description="Minimum classification applied to the downloaded "
1596//                         "files and required to know the existance of the source."))
1597//     data: str = odm.Optional(odm.Keyword(description="Data block sent during the URL call (Uses replace pattern)"))
1598//     failure_pattern: str = odm.Optional(odm.Keyword(
1599//         description="Pattern to find as a failure case when API return 200 OK on failures..."))
1600//     method: str = odm.Enum(values=['GET', 'POST'], default="GET", description="Method used to call the URL")
1601//     url: str = odm.Keyword(description="Url to fetch the file via SHA256 from (Uses replace pattern)")
1602//     replace_pattern: str = odm.Keyword(description="Pattern to replace in the URL with the SHA256")
1603//     headers: Dict[str, str] = odm.Mapping(odm.Keyword(), default={},
1604//                                           description="Headers used to connect to the URL")
1605//     proxies: Dict[str, str] = odm.Mapping(odm.Keyword(), default={},
1606//                                           description="Proxy used to connect to the URL")
1607//     verify: bool = odm.Boolean(default=True, description="Should the download function Verify SSL connections?")
1608
1609
1610// EXAMPLE_SHA256_SOURCE_VT = {
1611//     # This is an example on how this would work with VirusTotal
1612//     "name": "VirusTotal",
1613//     "url": r"https://www.virustotal.com/api/v3/files/{SHA256}/download",
1614//     "replace_pattern": r"{SHA256}",
1615//     "headers": {"x-apikey": "YOUR_KEY"},
1616// }
1617
1618// EXAMPLE_SHA256_SOURCE_MB = {
1619//     # This is an example on how this would work with Malware Bazaar
1620//     "name": "Malware Bazaar",
1621//     "url": r"https://mb-api.abuse.ch/api/v1/",
1622//     "headers": {"Content-Type": "application/x-www-form-urlencoded"},
1623//     "data": r"query=get_file&sha256_hash={SHA256}",
1624//     "method": "POST",
1625//     "replace_pattern": r"{SHA256}",
1626//     "failure_pattern": '"query_status": "file_not_found"'
1627// }
1628
1629
1630// @odm.model(index=False, store=False,
1631//            description="Minimum score value to get the specified verdict, otherwise the file is considered safe.")
1632// class Verdicts(odm.Model):
1633//     info: int = odm.Integer(description="Minimum score for the verdict to be Informational.")
1634//     suspicious: int = odm.Integer(description="Minimum score for the verdict to be Suspicious.")
1635//     highly_suspicious: int = odm.Integer(description="Minimum score for the verdict to be Highly Suspicious.")
1636//     malicious: int = odm.Integer(description="Minimum score for the verdict to be Malicious.")
1637
1638
1639// DEFAULT_VERDICTS = {
1640//     'info': 0,
1641//     'suspicious': 300,
1642//     'highly_suspicious': 700,
1643//     'malicious': 1000
1644// }
1645
1646#[derive(SerializeDisplay, DeserializeFromStr, strum::Display, strum::EnumString, Debug, Clone, Copy)]
1647// #[metadata_type(ElasticMeta)]
1648#[strum(serialize_all = "lowercase")]
1649pub enum TemporaryKeyType {
1650    Union,
1651    Overwrite,
1652}
1653
1654impl Default for TemporaryKeyType {
1655    fn default() -> Self {
1656        Self::Overwrite
1657    }
1658}
1659
1660
1661/// Default values for parameters for submissions that may be overridden on a per submission basis
1662#[derive(Serialize, Deserialize)]
1663#[serde(default)]
1664pub struct Submission {
1665    // /// How many extracted files may be added to a submission?
1666    // pub default_max_extracted: u32,
1667    // /// How many supplementary files may be added to a submission?
1668    // pub default_max_supplementary: u32,
1669    // /// Number of days submissions will remain in the system by default
1670    // pub dtl: u32,
1671    /// Number of days emptyresult will remain in the system
1672    pub emptyresult_dtl: u32,
1673    /// Maximum number of days submissions will remain in the system
1674    pub max_dtl: u32,
1675    /// Maximum files extraction depth
1676    pub max_extraction_depth: u32,
1677    /// Maximum size for files submitted in the system
1678    pub max_file_size: u64,
1679    /// Maximum length for each metadata values
1680    pub max_metadata_length: u32,
1681    /// Maximum length for each temporary data values
1682    pub max_temp_data_length: u32,
1683    // /// List of external source to fetch file via their SHA256 hashes
1684    // pub sha256_sources: Vec<Sha256Source>,
1685    // /// Tag types that show up in the submission summary
1686    // pub tag_types: TagTypes,
1687    // /// Minimum score value to get the specified verdict.
1688    // pub verdicts: Verdicts,
1689
1690    /// Set the operation that will be used to update values using this key in the temporary submission data.
1691    pub default_temporary_keys: HashMap<String, TemporaryKeyType>,
1692    pub temporary_keys: HashMap<String, TemporaryKeyType>,
1693}
1694
1695impl Default for Submission {
1696    fn default() -> Self {
1697        Self {
1698            // default_max_extracted: 500,
1699            // default_max_supplementary: 500,
1700            // dtl: 30,
1701            emptyresult_dtl: 5,
1702            max_dtl: 0,
1703            max_extraction_depth: 6,
1704            max_file_size: 104857600,
1705            max_metadata_length: 4096,
1706            max_temp_data_length: 4096,
1707            // sha256_sources: Default::default(),
1708            // tag_types: Default::default(),
1709            // verdicts: Default::default()
1710            default_temporary_keys: [
1711                ("passwords".to_owned(), TemporaryKeyType::Union),
1712                ("email_body".to_owned(), TemporaryKeyType::Union),
1713            ].into_iter().collect(),
1714            temporary_keys: Default::default()
1715        }
1716    }
1717}
1718
1719
1720// @odm.model(index=False, store=False, description="Configuration for connecting to a retrohunt service.")
1721// class Retrohunt(odm.Model):
1722//     enabled = odm.Boolean(default=False, description="Is the Retrohunt functionnality enabled on the frontend")
1723//     dtl: int = odm.Integer(default=30, description="Number of days retrohunt jobs will remain in the system by default")
1724//     max_dtl: int = odm.Integer(
1725//         default=0, description="Maximum number of days retrohunt jobs will remain in the system")
1726//     url = odm.Keyword(description="Base URL for service API")
1727//     api_key = odm.Keyword(description="Service API Key")
1728//     tls_verify = odm.Boolean(description="Should tls certificates be verified", default=True)
1729
1730
1731/// Assemblyline Deployment Configuration
1732#[derive(Serialize, Deserialize, Default)]
1733#[serde(default)]
1734pub struct Config {
1735    /// Classification information
1736    pub classification: Classification,
1737    // /// Authentication module configuration
1738    // pub auth: Auth,
1739    /// Core component configuration
1740    pub core: Core,
1741    /// Datastore configuration
1742    pub datastore: Datastore,
1743    // /// Datasources configuration
1744    // #[serde(default = "default_datasources")]
1745    // pub datasources: HashMap<String, Datasource>,
1746    /// Filestore configuration
1747    pub filestore: Filestore,
1748    /// Logging configuration
1749    pub logging: Logging,
1750    /// Service configuration
1751    pub services: Services,
1752    // /// System configuration
1753    // pub system: System,
1754    /// UI configuration parameters
1755    pub ui: UI,
1756    /// Options for how submissions will be processed
1757    pub submission: Submission,
1758    // /// Retrohunt configuration for the frontend and server
1759    // pub retrohunt: Option<Retrohunt>,
1760}
1761
1762