assemblyline-markings 0.2.0

Library for using access control strings with the Assemblyline malware analysis platform.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
//! Objects for parsing configuration data from assemblyline.

use std::collections::HashMap;

use serde::{Deserialize, Serialize};

use crate::errors::Errors;

/// This the default classification engine provided with Assemblyline,
/// it showcases all the different features of the classification engine
/// while providing a useful configuration
pub static DEFAULT_CLASSIFICATION_DATA: &str = r"
# Turn on/off classification enforcement. When this flag is off, this
#  completely disables the classification engine, any documents added while
#  the classification engine is off gets the default unrestricted value
enforce: false

# Turn on/off dynamic group creation. This feature allow you to dynamically create classification groups based on
#  features from the user.
dynamic_groups: false

# Set the type of dynamic groups to be used
#  email: groups will be based of the user's email domain
#  group: groups will be created out the the user's group values
#  all: groups will be created out of both the email domain and the group values
dynamic_groups_type: email

# List of Classification level:
#   Graded list were a smaller number is less restricted then an higher number.
levels:
  # List of alternate names for the current marking
  - aliases:
      - UNRESTRICTED
      - UNCLASSIFIED
      - U
      - TLP:W
      - TLP:WHITE
    # Stylesheet applied in the UI for the different levels
    css:
      # Name of the color scheme used for display (default, primary, secondary, success, info, warning, error)
      color: default
    # Description of the classification level
    description: Subject to standard copyright rules, TLP:CLEAR information may be distributed without restriction.
    # Interger value of the Classification level (higher is more classified)
    lvl: 100
    # Long name of the classification item
    name: TLP:CLEAR
    # Short name of the classification item
    short_name: TLP:C
  - aliases: []
    css:
      color: success
    description:
      Recipients may share TLP:GREEN information with peers and partner organizations
      within their sector or community, but not via publicly accessible channels. Information
      in this category can be circulated widely within a particular community. TLP:GREEN
      information may not be released outside of the community.
    lvl: 110
    name: TLP:GREEN
    short_name: TLP:G
  - aliases: []
    css:
      color: warning
    description:
      Recipients may only share TLP:AMBER information with members of their
      own organization and with clients or customers who need to know the information
      to protect themselves or prevent further harm.
    lvl: 120
    name: TLP:AMBER
    short_name: TLP:A
  - aliases:
      - RESTRICTED
    css:
      color: warning
    description:
      Recipients may only share TLP:AMBER+STRICT information with members of their
      own organization.
    lvl: 125
    name: TLP:AMBER+STRICT
    short_name: TLP:A+S

# List of required tokens:
#   A user requesting access to an item must have all the
#   required tokens the item has to gain access to it
required:
  - aliases: []
    description: Produced using a commercial tool with limited distribution
    name: COMMERCIAL
    short_name: CMR
    # The minimum classification level an item must have
    #   for this token to be valid. (optional)
    # require_lvl: 100
    # This is a token that is required but will display in the groups part
    #   of the classification string. (optional)
    # is_required_group: true

# List of groups:
#   A user requesting access to an item must be part of a least
#   of one the group the item is part of to gain access
groups:
  - aliases: []
    # This is a special flag that when set to true, if any groups are selected
    #   in a classification. This group will automatically be selected too. (optional)
    auto_select: true
    description: Employees of CSE
    name: CSE
    short_name: CSE
    # Assuming that this groups is the only group selected, this is the display name
    #   that will be used in the classification (that values has to be in the aliases
    #   of this group and only this group) (optional)
    # solitary_display_name: ANY

# List of subgroups:
#   A user requesting access to an item must be part of a least
#   of one the subgroup the item is part of to gain access
subgroups:
  - aliases: []
    description: Member of Incident Response team
    name: IR TEAM
    short_name: IR
  - aliases: []
    description: Member of the Canadian Centre for Cyber Security
    # This is a special flag that auto-select the corresponding group
    #   when this subgroup is selected (optional)
    require_group: CSE
    name: CCCS
    short_name: CCCS
    # This is a special flag that makes sure that none other then the
    #   corresponding group is selected when this subgroup is selected (optional)
    # limited_to_group: CSE

# Default restricted classification
restricted: TLP:A+S//CMR

# Default unrestricted classification:
#   When no classification are provided or that the classification engine is
#   disabled, this is the classification value each items will get
unrestricted: TLP:C";

/// parse classification config applying asssemblyline's defaults
pub fn ready_classification(config_data: Option<&str>) -> Result<ClassificationConfig, Errors> {
    if let Some(config_data) = config_data {
        // Load modifiers from the yaml config
        let yml_data: serde_yaml::mapping::Mapping = serde_yaml::from_str(config_data)?;
        let mut config: serde_yaml::Value = serde_yaml::from_str(DEFAULT_CLASSIFICATION_DATA)?;
        config.as_mapping_mut().unwrap().extend(yml_data);
        Ok(serde_yaml::from_value(config)?)
    } else {
        Ok(serde_yaml::from_str(DEFAULT_CLASSIFICATION_DATA)?)
    }
}


/// Define sources for dynamic groups
#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq)]
#[serde(rename_all="lowercase")]
pub enum DynamicGroupType {
    /// Draw classification groups from user email domains
    #[default]
    Email,
    /// Draw classification groups from user ldap groups
    Group,
    /// Draw classification groups from user email domains and ldap groups
    All,
}


/// A description of the configuration block used by assemblyline for classification schemes
#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct ClassificationConfig {
    /// Turn on/off classification enforcement. When this flag is off, this
    /// completely disables the classification engine, any documents added while
    /// the classification engine is off gets the default unrestricted value
    pub enforce: bool,

    /// Turn on/off dynamic group creation. This feature allow you to dynamically create classification groups based on
    ///  features from the user.
    pub dynamic_groups: bool,

    /// Set the type of dynamic groups to be used
    #[serde(default)]
    pub dynamic_groups_type: DynamicGroupType,

    /// List of Classification level.
    /// Graded list were a smaller number is less restricted then an higher number.
    pub levels: Vec<ClassificationLevel>,

    /// List of required tokens:
    /// A user requesting access to an item must have all the
    /// required tokens the item has to gain access to it
    pub required: Vec<ClassificationMarking>,

    /// List of groups:
    /// A user requesting access to an item must be part of a least
    /// of one the group the item is part of to gain access
    pub groups: Vec<ClassificationGroup>,

    /// List of subgroups:
    /// A user requesting access to an item must be part of a least
    /// of one the subgroup the item is part of to gain access
    pub subgroups: Vec<ClassificationSubGroup>,

    /// Default restricted classification
    pub restricted: String,

    /// Default unrestricted classification.
    /// When no classification are provided or that the classification engine is
    /// disabled, this is the classification value each items will get
    pub unrestricted: String,
}

/// A category of data delineating access
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct ClassificationLevel {
    /// List of alternate names for the current marking
    #[serde(default)]
    pub aliases: Vec<NameString>,

    /// Stylesheet applied in the UI for the different levels
    #[serde(default="default_css")]
    pub css: HashMap<String, String>,

    /// Description of the classification level
    #[serde(default="default_description")]
    pub description: String,

    /// Interger value of the Classification level (higher is more classified)
    pub lvl: i32,

    /// Long name of the classification item
    pub name: NameString,

    /// Short name of the classification item
    pub short_name: NameString,

    /// Should the classification be skipped building UI options
    #[serde(default)]
    pub is_hidden: bool,

    // #[serde(flatten)]
    // currently planning to static define other fields as optional, making other_fields unneeded
    // pub other_fields: HashMap<String, serde_value::Value>,
}

impl ClassificationLevel {
    /// construct a classification level from its rank (higher is more restricted) and how it should be displayed
    pub fn new(lvl: i32, short_name: &str, name: &str, aliases: Vec<&str>) -> Self {
        ClassificationLevel {
            aliases: aliases.into_iter().map(|x|x.parse().unwrap()).collect(),
            css: default_css(),
            description: default_description(),
            lvl,
            name: name.parse().unwrap(),
            short_name: short_name.parse().unwrap(),
            is_hidden: false,
        }
    }

    /// Get all of the unique names used by this item
    pub fn unique_names(&self) -> Vec<NameString> {
        let mut names = vec![self.name.clone(), self.short_name.clone()];
        names.extend(self.aliases.iter().cloned());
        names.sort_unstable();
        names.dedup();
        return names
    }
}

/// Get the CSS value to be used for classification levels when none is configured
fn default_css() -> HashMap<String, String> { [("color".to_owned(), "default".to_owned()), ].into_iter().collect() }

/// Get the description to be used on any description field that is not defined
fn default_description() -> String {"N/A".to_owned()}

/// A control or dissemination marking
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct ClassificationMarking {
    /// List of alternate names for the current marking
    #[serde(default)]
    pub aliases: Vec<NameString>,

    /// Long form description of marking
    #[serde(default="default_description")]
    pub description: String,

    /// Long form canonical name of marking
    // #[serde(deserialize_with="deserialize_normalized_name")]
    pub name: NameString,

    /// Short form canonical name of marking
    // #[serde(deserialize_with="deserialize_normalized_name")]
    pub short_name: NameString,

    /// The minimum classification level an item must have for this token to be valid. (optional)
    #[serde(default)]
    pub require_lvl: Option<i32>,

    /// This is a token that is required but will display in the groups part
    /// of the classification string. (optional)
    #[serde(default)]
    pub is_required_group: bool,

    /// Should the marking be skipped building UI options
    #[serde(default)]
    pub is_hidden: bool,
}

impl ClassificationMarking {
    /// Create a marking from canonical names and aliases
    pub fn new(short_name: &str, name: &str, aliases: Vec<&str>) -> Self {
        Self {
            aliases: aliases.into_iter().map(|x|x.parse().unwrap()).collect(),
            description: default_description(),
            name: name.parse().unwrap(),
            short_name: short_name.parse().unwrap(),
            require_lvl: None,
            is_required_group: false,
            is_hidden: false,
        }
    }

    /// Create a marking from canonical names that is a required group
    pub fn new_required(short_name: &str, name: &str) -> Self {
        let mut new = Self::new(short_name, name, vec![]);
        new.is_required_group = true;
        new
    }

    /// Get all of the unique names used by this item
    pub fn unique_names(&self) -> Vec<NameString> {
        let mut names = vec![self.name.clone(), self.short_name.clone()];
        names.extend(self.aliases.iter().cloned());
        names.sort_unstable();
        names.dedup();
        return names
    }
}

/// A group granted access to an object
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct ClassificationGroup {
    /// List of alternate names for this group
    #[serde(default)]
    pub aliases: Vec<NameString>,

    /// This is a special flag that when set to true, if any groups are selected
    ///   in a classification. This group will automatically be selected too. (optional)
    #[serde(default)]
    pub auto_select: bool,

    /// Long form description of marking
    #[serde(default="default_description")]
    pub description: String,

    /// Long form canonical name of marking
    pub name: NameString,

    /// Short form canonical name of marking
    pub short_name: NameString,

    /// Assuming that this groups is the only group selected, this is the display name
    /// that will be used in the classification (that values has to be in the aliases
    /// of this group and only this group) (optional)
    #[serde(default)]
    pub solitary_display_name: Option<NameString>,

    /// Should the marking be skipped building UI options
    #[serde(default)]
    pub is_hidden: bool,
}

impl ClassificationGroup {
    /// Create access control group from canonical names
    pub fn new(short_name: &str, name: &str) -> Self {
        Self {
            name: name.parse().unwrap(),
            short_name: short_name.parse().unwrap(),
            aliases: vec![],
            auto_select: false,
            description: default_description(),
            solitary_display_name: None,
            is_hidden: false,
        }
    }

    /// Create access control group from canonical names that has a special display form when presented alone
    pub fn new_solitary(short_name: &str, name: &str, solitary_display: &str) -> Self {
        let mut new = Self::new(short_name, name);
        new.solitary_display_name = Some(solitary_display.parse().unwrap());
        return new
    }
}

/// A subgroup granted access to an object
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct ClassificationSubGroup {
    /// List of alternate names for the current marking
    #[serde(default)]
    pub aliases: Vec<NameString>,

    /// This is a special flag that when set to true, if any groups are selected
    ///   in a classification. This group will automatically be selected too. (optional)
    #[serde(default)]
    pub auto_select: bool,

    /// Long form description of marking
    #[serde(default="default_description")]
    pub description: String,

    /// Long form canonical name of marking
    pub name: NameString,

    /// Short form canonical name of marking
    pub short_name: NameString,

    // /// Assuming that this groups is the only group selected, this is the display name
    // /// that will be used in the classification (that values has to be in the aliases
    // /// of this group and only this group) (optional)
    // ///
    // /// Loaded in the python version, but not actually used
    // #[serde(default)]
    // pub solitary_display_name: Option<String>,

    /// This is a special flag that auto-select the corresponding group when
    /// this subgroup is selected (optional)
    #[serde(default)]
    pub require_group: Option<NameString>,

    /// This is a special flag that makes sure that none other then the
    /// corresponding group is selected when this subgroup is selected (optional)
    #[serde(default)]
    pub limited_to_group: Option<NameString>,

    /// Should the marking be skipped building UI options
    #[serde(default)]
    pub is_hidden: bool,
}


impl ClassificationSubGroup {
    /// Create a new subgroup with aliases
    pub fn new_aliased(short_name: &str, name: &str, aliases: Vec<&str>) -> Self {
        Self {
            short_name: short_name.parse().unwrap(),
            name: name.parse().unwrap(),
            aliases: aliases.iter().map(|item|item.parse().unwrap()).collect(),
            auto_select: false,
            description: default_description(),
            require_group: None,
            limited_to_group: None,
            is_hidden: false,
        }
    }

    /// create a new subgroup with required group
    pub fn new_with_required(short_name: &str, name: &str, required: &str) -> Self {
        let mut new = Self::new_aliased(short_name, name, vec![]);
        new.require_group = Some(required.parse().unwrap());
        return new
    }

    /// Create a new subgroup limited in access to a given group
    pub fn new_with_limited(short_name: &str, name: &str, limited: &str) -> Self {
        let mut new = Self::new_aliased(short_name, name, vec![]);
        new.limited_to_group = Some(limited.parse().unwrap());
        return new
    }
}


/// A string restricted to the conditions required for the name of a classification element.
/// Non-zero length and uppercase.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct NameString(String);

impl core::ops::Deref for NameString {
    type Target = str;

    fn deref(&self) -> &Self::Target {
        self.0.as_str()
    }
}

impl std::str::FromStr for NameString {
    type Err = Errors;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let data = s.trim().to_uppercase();
        if data.is_empty() {
            return Err(Errors::ClassificationNameEmpty)
        }
        Ok(Self(data))
    }
}

impl core::fmt::Display for NameString {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(&self.0)
    }
}

impl NameString {
    /// Access the raw string data behind this object
    pub fn as_str(&self) -> &str {
        &self.0
    }
}

impl<'de> Deserialize<'de> for NameString {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where D: serde::Deserializer<'de> {
        let data = String::deserialize(deserializer)?;
        data.parse().map_err(serde::de::Error::custom)
    }
}

impl Serialize for NameString {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer {
        self.0.serialize(serializer)
    }
}

#[test]
fn check_default_configurations() {
    let default_config = ready_classification(None).unwrap();
    assert!(!default_config.enforce);

    println!("{:?}", serde_yaml::to_value(DEFAULT_CLASSIFICATION_DATA).unwrap());

    let config = ready_classification(Some("enforce: true")).unwrap();
    assert!(config.enforce);

    use crate::classification::ClassificationParser;
    let ce = ClassificationParser::new(default_config).unwrap();

    assert_eq!(ce.normalize_classification("ABC123").unwrap(), ce.unrestricted());
}