assemblyline_models/
config.rs

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