use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use crate::errors::Errors;
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";
pub fn ready_classification(config_data: Option<&str>) -> Result<ClassificationConfig, Errors> {
if let Some(config_data) = config_data {
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)?)
}
}
#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq)]
#[serde(rename_all="lowercase")]
pub enum DynamicGroupType {
#[default]
Email,
Group,
All,
}
#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct ClassificationConfig {
pub enforce: bool,
pub dynamic_groups: bool,
#[serde(default)]
pub dynamic_groups_type: DynamicGroupType,
pub levels: Vec<ClassificationLevel>,
pub required: Vec<ClassificationMarking>,
pub groups: Vec<ClassificationGroup>,
pub subgroups: Vec<ClassificationSubGroup>,
pub restricted: String,
pub unrestricted: String,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct ClassificationLevel {
#[serde(default)]
pub aliases: Vec<NameString>,
#[serde(default="default_css")]
pub css: HashMap<String, String>,
#[serde(default="default_description")]
pub description: String,
pub lvl: i32,
pub name: NameString,
pub short_name: NameString,
#[serde(default)]
pub is_hidden: bool,
}
impl ClassificationLevel {
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,
}
}
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
}
}
fn default_css() -> HashMap<String, String> { [("color".to_owned(), "default".to_owned()), ].into_iter().collect() }
fn default_description() -> String {"N/A".to_owned()}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct ClassificationMarking {
#[serde(default)]
pub aliases: Vec<NameString>,
#[serde(default="default_description")]
pub description: String,
pub name: NameString,
pub short_name: NameString,
#[serde(default)]
pub require_lvl: Option<i32>,
#[serde(default)]
pub is_required_group: bool,
#[serde(default)]
pub is_hidden: bool,
}
impl ClassificationMarking {
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,
}
}
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
}
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
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct ClassificationGroup {
#[serde(default)]
pub aliases: Vec<NameString>,
#[serde(default)]
pub auto_select: bool,
#[serde(default="default_description")]
pub description: String,
pub name: NameString,
pub short_name: NameString,
#[serde(default)]
pub solitary_display_name: Option<NameString>,
#[serde(default)]
pub is_hidden: bool,
}
impl ClassificationGroup {
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,
}
}
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
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct ClassificationSubGroup {
#[serde(default)]
pub aliases: Vec<NameString>,
#[serde(default)]
pub auto_select: bool,
#[serde(default="default_description")]
pub description: String,
pub name: NameString,
pub short_name: NameString,
#[serde(default)]
pub require_group: Option<NameString>,
#[serde(default)]
pub limited_to_group: Option<NameString>,
#[serde(default)]
pub is_hidden: bool,
}
impl ClassificationSubGroup {
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,
}
}
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
}
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
}
}
#[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 {
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());
}