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