initial commit

This commit is contained in:
avitex 2021-03-02 15:17:16 +11:00
commit 22a4e9fbf8
Signed by: avitex
GPG Key ID: 38C76CBF3749D62C
59 changed files with 2111 additions and 0 deletions

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
/target
Cargo.lock
# STORM
/dist

29
Cargo.toml Normal file
View File

@ -0,0 +1,29 @@
[package]
name = "cyberstorm"
version = "0.1.0"
authors = ["avitex <avitex@wfxlabs.com>"]
edition = "2018"
publish = false
build = "build.rs"
[dependencies]
zc = "0.4"
serde = { version = "1.0", features = ["derive"] }
toml = "0.5"
async-trait = "0.1"
strum = { version = "0.20", features = ["derive"] }
roxmltree = "0.14"
tokio = { version = "1", features = ["rt-multi-thread", "fs", "macros"] }
validator = { version = "0.12", features = ["derive"] }
thiserror = "1.0"
tera = "1.6"
anyhow = "1.0"
regex = "1"
lazy_static = "1"
structopt = "0.3"
phf = "0.8.0"
includedir = "0.6"
[build-dependencies]
includedir_codegen = "0.6"
glob = "0.3"

84
README.md Normal file
View File

@ -0,0 +1,84 @@
# STORM
An angry cloud.
A registry of how to protect a system.
## Tags
Tags are limited to `a-z`, `0-9`, or `-` characters and can be hierarchically separated via `.`.
For example when referring to a MITRE ATT&CK technique we could use `attack.t0001`.
## Domains
| Domain | Model | Description |
| ---------- | ------------- | ----------------------------------------------------------------------------- |
| [Source] | | Sourcing of intelligence for improving observation, reaction and mitigation |
| | Intelligence | Feed or result of a query used to satisfy intelligence requirements |
| | Requirement | Collection of intelligence used by actions, detections and mitigations |
| | Provider | An internal or external supplier of intelligence |
| [Threat] | | Modelling and simulations of threat actor tactics, techniques and software |
| | Tactic | High-level categorization of threat actor behaviour |
| | Technique | Specific description of threat actor behaviour |
| | Software | Code/utilities/tools used in conducting threat actor behaviour |
| | Simulation | Generating threat behaviour for testing observation, reaction and mitigations |
| [Observe] | | Events, definitions and configuration for observing threat actor behaviour |
| | Event | Observable items use to detect and respond to threat actor behaviour |
| | Detection | Rule used to detect threat actor behaviour mapped to threat actor behaviour |
| | Provider | Software or systems that are capable of producing events |
| | Configuration | Configuration applied to a provider enabling it to produce events |
| [React] | | Structured responses to observed threat behaviour |
| | Stage | Phase of a response to observed threat behaviour |
| | Action | An atomic human action assigned to an response stage |
| | Playbook | Composition of response actions for a given context |
| [Mitigate] | | Proactive and reactive strategies to address address threats |
| | Strategy | Composition of platforms and configurations to address a threat |
| | Platform | Platform that is capable of mitigating threats with or without configuration |
| | Configuration | Configuration applied to a platform enabling it to mitigate threats |
### Source
How do you what is happening outside of your system or if something you saw
is elsewhere considered a threat?
A source is an entity that provides an external perspective and knowledge of
threats, techniques, tools, mitigations that can be pulled or queried.
#### Provider
#### Requirement
#### Intelligence
### Threat
### Observe
### React
### Mitigate
source/
provider `SPR#####`
requirement `SRT#####`
intelligence `SIE#####`
threat/
tactic `TTC#####`
software `TSE#####`
technique `TTE#####`
deficiency
simulation `TSN#####`
observe/
event `OET#####`
detection `ODN#####`
provider `OPR#####`
configuration `OCG#####`
react/
stage `RSE#####`
action `RAN#####`
playbook `RPK#####`
mitigate/
strategy `MSY#####`
platform `MPM#####`
configuration `MCG#####`

14
book.toml Normal file
View File

@ -0,0 +1,14 @@
[book]
authors = ["avitex"]
language = "en"
multilingual = false
src = "book"
title = "STORM"
[build]
create-missing = false
build-dir = "dist"
[output.html.fold]
enable = true
level = 0

6
book/.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
/SUMMARY.md
/source/**/*.md
/threat/**/*.md
/observe/**/*.md
/react/**/*.md
/mitigate/**/*.md

49
book/macros.tera Normal file
View File

@ -0,0 +1,49 @@
{% macro references(refs) %}
{%- if refs | length == 0 %}No references{% endif -%}
{%- for ref in refs -%}
- {{ ref | autolink }}
{% endfor -%}
{% endmacro references %}
{% macro title(title, id) -%}
{{ title }} <small>([edit]({{ id | domain_id_link(for="edit") }}))</small>
{%- endmacro details_next %}
{% macro details(id, name) -%}
| Title | {{ name }} |
|:---------------------------:|:------------------------|
{{ self::details_next(title="ID", value=id)}}
{%- endmacro details %}
{% macro details_next(title, value) -%}
| **{{ title }}** | {{ value }} |
{%- endmacro details_next %}
{% macro details_authors(authors) -%}
{{ self::details_next(title="Authors", value=authors | join )}}
{%- endmacro details_next %}
{% macro details_tags(tags) -%}
{% if tags | length == 0 -%}
{{ self::details_next(title="Tags", value="No tags") }}
{%- else -%}
{{ self::details_next(title="Tags", value=tags | join) }}
{%- endif %}
{%- endmacro details_next %}
{% macro content(content) %}
{%- set trimmed = content | trim -%}
{%- if trimmed | length == 0 %}No description{% else %}{{ trimmed }}{% endif -%}
{% endmacro details_next %}
{% macro name_and_id_link(value) -%}
{{ value["doc"]["name"] }} ([{{ value["id"] }}]({{ value["id"] | domain_id_link }}))
{%- endmacro name_and_id_link %}
{% macro summary_list(instances) -%}
| ID | Name |
|:---------------------------:|:------------------------|
{% for item in instances -%}
| [{{ item.id }}]({{ item.id | domain_id_link }}) | {{ item.name }} |
{% endfor %}
{%- endmacro summary_list %}

View File

@ -0,0 +1,22 @@
{% import "macros.tera" as macros %}
# {{ macros::title(title=doc.name, id=id) }}
{{ macros::details(id=id, name=doc.name) }}
{%- if doc.WindowsEvent %}
{{ macros::details_next(title="Type", value="Windows event") }}
{% endif -%}
{{ macros::details_next(title="Description", value=doc.description) }}
## Description
{{ macros::content(content=doc.content) }}
{% if doc.WindowsEvent %}
## Samples
{% for sample in doc.WindowsEvent.sample %}
```xml
{{ sample.xml }}
```
{% endfor %}
{% endif %}

7
book/observe/event.tera Normal file
View File

@ -0,0 +1,7 @@
{% import "macros.tera" as macros %}
# Event
Observable items use to detect and respond to threat actor behaviour.
{{ macros::summary_list(instances=instances) }}

View File

@ -0,0 +1,17 @@
{% import "macros.tera" as macros %}
{% set stage = doc.stage | get_doc %}
# {{ macros::title(title=doc.name, id=id) }}
{{ macros::details(id=id, name=doc.name) }}
{{ macros::details_next(title="Stage", value=macros::name_and_id_link(value=stage)) }}
{{ macros::details_next(title="Description", value=doc.description) }}
{{ macros::details_tags(tags=doc.tags) }}
## Description
{{ macros::content(content=doc.content) }}
## References
{{ macros::references(refs=doc.references) }}

5
book/react/action.tera Normal file
View File

@ -0,0 +1,5 @@
{% import "macros.tera" as macros %}
# Action
{{ macros::summary_list(instances=instances) }}

View File

@ -0,0 +1,10 @@
{% import "macros.tera" as macros %}
# {{ macros::title(title=doc.name, id=id) }}
{{ macros::details(id=id, name=doc.name) }}
{{ macros::details_next(title="Description", value=doc.description) }}
## Description
{{ macros::content(content=doc.content) }}

7
book/react/stage.tera Normal file
View File

@ -0,0 +1,7 @@
{% import "macros.tera" as macros %}
# Stage
Phase of a response to observed threat behaviour.
{{ macros::summary_list(instances=instances) }}

View File

@ -0,0 +1,14 @@
{% import "macros.tera" as macros %}
# {{ macros::title(title=doc.name, id=id) }}
{{ macros::details(id=id, name=doc.name) }}
{{ macros::details_next(title="Provider", value=doc.provider) }}
## Description
{{ macros::content(content=doc.content) }}
## References
{{ macros::references(refs=doc.references) }}

View File

@ -0,0 +1,7 @@
{% import "macros.tera" as macros %}
# Intelligence
Feed or result of a query used to satisfy intelligence requirements.
{{ macros::summary_list(instances=instances) }}

View File

@ -0,0 +1,13 @@
{% import "macros.tera" as macros %}
# {{ macros::title(title=doc.name, id=id) }}
{{ macros::details(id=id, name=doc.name) }}
## Description
{{ macros::content(content=doc.content) }}
## References
{{ macros::references(refs=doc.references) }}

View File

@ -0,0 +1,7 @@
{% import "macros.tera" as macros %}
# Provider
An internal or external supplier of intelligence.
{{ macros::summary_list(instances=instances) }}

View File

@ -0,0 +1,13 @@
{% import "macros.tera" as macros %}
# {{ macros::title(title=doc.name, id=id) }}
{{ macros::details(id=id, name=doc.name) }}
## Description
{{ macros::content(content=doc.content) }}
## References
{{ macros::references(refs=doc.references) }}

View File

@ -0,0 +1,7 @@
{% import "macros.tera" as macros %}
# Requirement
Collection of intelligence used by actions, detections and mitigations.
{{ macros::summary_list(instances=instances) }}

16
book/summary.tera Normal file
View File

@ -0,0 +1,16 @@
# Summary
{%- set last_domain = "" -%}
{%- set last_model = "" -%}
{%- for item in summary -%}
{%- set id_parts = item.id | domain_id -%}
{%- if last_domain != id_parts.domain -%}
{%- set_global last_domain = id_parts.domain %}
# {{ id_parts.domain | capitalize }}
{%- endif -%}
{%- if last_model != id_parts.model -%}
{%- set_global last_model = id_parts.model %}
- [{{ id_parts.model | capitalize }}](.{{ item.id | domain_id_link(for="model") }})
{%- endif %}
- [{{ item.name }}](.{{ item.id | domain_id_link }})
{%- endfor -%}

13
build.rs Normal file
View File

@ -0,0 +1,13 @@
use glob::glob;
use includedir_codegen::Compression;
fn main() {
let mut templates = includedir_codegen::start("BASE_TEMPLATES");
for path_result in glob("book/**/*.tera").unwrap() {
let path = path_result.unwrap();
templates.add_file(path, Compression::Gzip).unwrap();
}
templates.build("mdbook-templates.rs").unwrap();
}

1
registry/config.toml Normal file
View File

@ -0,0 +1 @@
edit-link = "https://localhost/registry/{{domain}}/{{model}}/{{model_id}}.md"

View File

@ -0,0 +1,3 @@
+++
+++

View File

@ -0,0 +1,3 @@
+++
+++

View File

@ -0,0 +1,3 @@
+++
+++

View File

@ -0,0 +1,3 @@
+++
+++

View File

@ -0,0 +1,3 @@
+++
+++

View File

@ -0,0 +1,43 @@
+++
name = "Some event"
description = "Some description"
[[WindowsEvent.sample]]
xml = """
<Event xmlns="http://schemas.microsoft.com/win/2004/08/events/event">
<System>
<Provider Name="Microsoft-Windows-Security-Auditing" Guid="{54849625-5478-4994-A5BA-3E3B0328C30D}" />
<EventID>4688</EventID>
<Version>2</Version>
<Level>0</Level>
<Task>13312</Task>
<Opcode>0</Opcode>
<Keywords>0x8020000000000000</Keywords>
<TimeCreated SystemTime="2015-11-12T02:24:52.377352500Z" />
<EventRecordID>2814</EventRecordID>
<Correlation />
<Execution ProcessID="4" ThreadID="400" />
<Channel>Security</Channel>
<Computer>WIN-GG82ULGC9GO.contoso.local</Computer>
<Security />
</System>
<EventData>
<Data Name="SubjectUserSid">S-1-5-18</Data>
<Data Name="SubjectUserName">WIN-GG82ULGC9GO$</Data>
<Data Name="SubjectDomainName">CONTOSO</Data>
<Data Name="SubjectLogonId">0x3e7</Data>
<Data Name="NewProcessId">0x2bc</Data>
<Data Name="NewProcessName">C:\\Windows\\System32\\rundll32.exe</Data>
<Data Name="TokenElevationType">%%1938</Data>
<Data Name="ProcessId">0xe74</Data>
<Data Name="TargetUserSid">S-1-5-21-1377283216-344919071-3415362939-1104</Data>
<Data Name="TargetUserName">dadmin</Data>
<Data Name="TargetDomainName">CONTOSO</Data>
<Data Name="TargetLogonId">0x4a5af0</Data>
<Data Name="ParentProcessName">C:\\Windows\\explorer.exe</Data>
<Data Name="MandatoryLabel">S-1-16-8192</Data>
</EventData>
</Event>
"""
+++
Something about the event

View File

@ -0,0 +1,3 @@
+++
+++

View File

@ -0,0 +1,6 @@
+++
name = 'Set up a centralized long-term log storage'
stage = 'RSE00001'
description = 'Set up a centralized long-term log storage. This is one of the most critical problems companies have nowadays. Even if there is such a system, in most of the cases it stores irrelevant data or has too small retention period'
+++

View File

@ -0,0 +1,4 @@
+++
name = "Preparation"
description = "Get prepared for a security incident"
+++

View File

@ -0,0 +1,4 @@
+++
name = "Identification"
description = "Gather information about a threat that has triggered a security incident, its TTPs, and affected assets"
+++

View File

@ -0,0 +1,4 @@
+++
name = "Containment"
description = "Prevent a threat from achieving its objectives and/or spreading around an environment"
+++

View File

@ -0,0 +1,4 @@
+++
name = "Eradication"
description = "Remove a threat from an environment"
+++

View File

@ -0,0 +1,4 @@
+++
name = "Recovery"
description = "Recover from the incident and return all the assets back to normal operation"
+++

View File

@ -0,0 +1,4 @@
+++
name = "Lessons Learned"
description = "Discover how to improve the Incident Response process and implement the improvements"
+++

View File

@ -0,0 +1,7 @@
+++
name = "DomainTools Whois Lookup"
provider = "SPR0001"
references = [
"https://whois.domaintools.com/"
]
+++

View File

@ -0,0 +1,3 @@
+++
name = "DomainTools"
+++

View File

@ -0,0 +1,3 @@
+++
name = "VirusTotal"
+++

View File

@ -0,0 +1,6 @@
+++
name = "OSINT assessment of IPv4/IPv6 address"
intelligence = [
"SIE0001",
]
+++

View File

@ -0,0 +1,6 @@
+++
name = "OSINT assessment of domain"
intelligence = [
"SIE0001",
]
+++

View File

@ -0,0 +1,3 @@
+++
name = "OSINT assessment of MD5 file hash"
+++

View File

@ -0,0 +1,3 @@
+++
name = "OSINT assessment of SHA256 file hash"
+++

73
src/document.rs Normal file
View File

@ -0,0 +1,73 @@
use std::fmt;
use std::str::FromStr;
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(Debug, Error)]
pub enum MetaError {
#[error("{0}")]
Parse(toml::de::Error),
#[error("meta data not present")]
NotPresent,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Document<M> {
#[serde(flatten)]
pub meta: M,
pub content: String,
}
impl<M> FromStr for Document<M>
where
M: DeserializeOwned,
{
type Err = MetaError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
parse_str_parts(s).map(|(meta, content)| Self {
meta,
content: content.to_owned(),
})
}
}
impl<M> fmt::Display for Document<M>
where
M: Serialize,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "+++")?;
writeln!(f, "{}", toml::to_string(&self.meta).unwrap())?;
writeln!(f, "\n+++")?;
f.write_str(self.content.as_str())
}
}
impl<M> Document<M>
where
M: DeserializeOwned,
{
pub fn parse_str_meta(s: &str) -> Result<M, MetaError> {
parse_str_parts(s).map(|(meta, _)| meta)
}
}
fn parse_str_parts<M>(s: &str) -> Result<(M, &str), MetaError>
where
M: DeserializeOwned,
{
if let Some(s) = s.strip_prefix("+++") {
let mut parts = s.splitn(2, "\n+++");
match (parts.next(), parts.next()) {
(Some(meta), Some(content)) => toml::from_str(meta)
.map(|meta| (meta, content))
.map_err(MetaError::Parse),
_ => Err(MetaError::NotPresent),
}
} else {
Err(MetaError::NotPresent)
}
}

236
src/domains/common.rs Normal file
View File

@ -0,0 +1,236 @@
use std::fmt;
use std::num::{NonZeroU16, ParseIntError};
use std::str::FromStr;
use serde::de::{DeserializeOwned, Deserializer, Error};
use serde::ser::Serializer;
use serde::{Deserialize, Serialize};
use strum::{EnumString, IntoStaticStr};
pub use toml::Value;
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct References(Vec<String>);
pub trait DomainModel: Send + Serialize + DeserializeOwned {
fn kind() -> DomainModelKind;
fn name(&self) -> &str;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, EnumString, IntoStaticStr)]
#[strum(serialize_all = "snake_case")]
pub enum DomainKind {
Source,
Threat,
Observe,
React,
Mitigate,
}
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord, EnumString, IntoStaticStr)]
pub enum DomainModelKind {
#[strum(serialize = "SIE")]
SourceIntelligence,
#[strum(serialize = "SPR")]
SourceProvider,
#[strum(serialize = "SRT")]
SourceRequirement,
#[strum(serialize = "TTC")]
ThreatTactic,
#[strum(serialize = "TTE")]
ThreatTechnique,
#[strum(serialize = "TSE")]
ThreatSoftware,
#[strum(serialize = "TSN")]
ThreatSimulation,
#[strum(serialize = "OET")]
ObserveEvent,
#[strum(serialize = "ODN")]
ObserveDetection,
#[strum(serialize = "OPR")]
ObserveProvider,
#[strum(serialize = "OCN")]
ObserveConfiguration,
#[strum(serialize = "RSE")]
ReactStage,
#[strum(serialize = "RAN")]
ReactAction,
#[strum(serialize = "RPK")]
ReactPlaybook,
#[strum(serialize = "MSY")]
MitigateStrategy,
#[strum(serialize = "MPM")]
MitigatePlatform,
#[strum(serialize = "MCN")]
MitigateConfiguration,
}
impl DomainModelKind {
pub fn domain(&self) -> DomainKind {
match self {
DomainModelKind::SourceIntelligence => DomainKind::Source,
DomainModelKind::SourceProvider => DomainKind::Source,
DomainModelKind::SourceRequirement => DomainKind::Source,
DomainModelKind::ThreatTactic => DomainKind::Threat,
DomainModelKind::ThreatTechnique => DomainKind::Threat,
DomainModelKind::ThreatSoftware => DomainKind::Threat,
DomainModelKind::ThreatSimulation => DomainKind::Threat,
DomainModelKind::ObserveEvent => DomainKind::Observe,
DomainModelKind::ObserveDetection => DomainKind::Observe,
DomainModelKind::ObserveProvider => DomainKind::Observe,
DomainModelKind::ObserveConfiguration => DomainKind::Observe,
DomainModelKind::ReactStage => DomainKind::React,
DomainModelKind::ReactAction => DomainKind::React,
DomainModelKind::ReactPlaybook => DomainKind::React,
DomainModelKind::MitigateStrategy => DomainKind::Mitigate,
DomainModelKind::MitigatePlatform => DomainKind::Mitigate,
DomainModelKind::MitigateConfiguration => DomainKind::Mitigate,
}
}
pub fn parts(self) -> (&'static str, &'static str) {
match self {
DomainModelKind::SourceIntelligence => ("source", "intelligence"),
DomainModelKind::SourceProvider => ("source", "provider"),
DomainModelKind::SourceRequirement => ("source", "requirement"),
DomainModelKind::ThreatTactic => ("threat", "tactic"),
DomainModelKind::ThreatTechnique => ("threat", "technique"),
DomainModelKind::ThreatSoftware => ("threat", "software"),
DomainModelKind::ThreatSimulation => ("threat", "simulation"),
DomainModelKind::ObserveEvent => ("observe", "event"),
DomainModelKind::ObserveDetection => ("observe", "detection"),
DomainModelKind::ObserveProvider => ("observe", "provider"),
DomainModelKind::ObserveConfiguration => ("observe", "configuration"),
DomainModelKind::ReactStage => ("react", "stage"),
DomainModelKind::ReactAction => ("react", "action"),
DomainModelKind::ReactPlaybook => ("react", "playbook"),
DomainModelKind::MitigateStrategy => ("mitigate", "strategy"),
DomainModelKind::MitigatePlatform => ("mitigate", "platform"),
DomainModelKind::MitigateConfiguration => ("mitigate", "configuration"),
}
}
}
///////////////////////////////////////////////////////////////////////////////
#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Copy)]
pub struct ModelId {
pub major: u16,
pub minor: Option<NonZeroU16>,
}
impl ModelId {
pub fn new(major: u16) -> Self {
Self { major, minor: None }
}
pub fn is_example(self) -> bool {
self.major == 0
}
}
impl fmt::Display for ModelId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:0>5}", self.major)?;
if let Some(minor) = self.minor {
write!(f, ".{:0>3}", minor)?;
}
Ok(())
}
}
impl FromStr for ModelId {
type Err = ParseIntError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut parts = s.splitn(2, '.');
let major = parts.next().unwrap().parse()?;
let minor = parts.next().map(FromStr::from_str).transpose()?;
Ok(Self { major, minor })
}
}
impl Serialize for ModelId {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.collect_str(self)
}
}
impl<'de> Deserialize<'de> for ModelId {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = <&str>::deserialize(deserializer)?;
s.parse().map_err(D::Error::custom)
}
}
///////////////////////////////////////////////////////////////////////////////
#[derive(Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Copy)]
pub struct DomainId {
pub kind: DomainModelKind,
pub model_id: ModelId,
}
impl DomainId {
pub fn new(kind: DomainModelKind, model_id: ModelId) -> Self {
Self { kind, model_id }
}
}
impl fmt::Debug for DomainId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("Id")
.field(&format_args!("{}", self))
.finish()
}
}
impl fmt::Display for DomainId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}{}", <&'static str>::from(&self.kind), self.model_id)
}
}
impl FromStr for DomainId {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.len() > 3 && s.is_char_boundary(3) {
let (kind, model_id) = s.split_at(3);
if let (Ok(kind), Ok(model_id)) =
(DomainModelKind::from_str(kind), ModelId::from_str(model_id))
{
return Ok(Self { kind, model_id });
}
}
Err("invalid storm id")
}
}
impl Serialize for DomainId {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.collect_str(self)
}
}
impl<'de> Deserialize<'de> for DomainId {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = <&str>::deserialize(deserializer)?;
s.parse().map_err(D::Error::custom)
}
}

48
src/domains/mitigate.rs Normal file
View File

@ -0,0 +1,48 @@
use serde::{Deserialize, Serialize};
use crate::domains::common::*;
#[derive(Debug, Serialize, Deserialize)]
pub struct Strategy {
pub name: String,
}
impl DomainModel for Strategy {
fn kind() -> DomainModelKind {
DomainModelKind::MitigateStrategy
}
fn name(&self) -> &str {
self.name.as_str()
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Platform {
pub name: String,
}
impl DomainModel for Platform {
fn kind() -> DomainModelKind {
DomainModelKind::MitigatePlatform
}
fn name(&self) -> &str {
self.name.as_str()
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Configuration {
pub name: String,
}
impl DomainModel for Configuration {
fn kind() -> DomainModelKind {
DomainModelKind::MitigateConfiguration
}
fn name(&self) -> &str {
self.name.as_str()
}
}

54
src/domains/mod.rs Normal file
View File

@ -0,0 +1,54 @@
use common::DomainModelKind;
use crate::document::Document;
pub mod common;
pub mod mitigate;
pub mod observe;
pub mod react;
pub mod source;
pub mod threat;
pub enum GenericDocument {
SourceIntelligence(Document<source::Intelligence>),
SourceProvider(Document<source::Provider>),
SourceRequirement(Document<source::Requirement>),
ThreatTactic(Document<threat::Tactic>),
ThreatTechnique(Document<threat::Technique>),
ThreatSoftware(Document<threat::Software>),
ThreatSimulation(Document<threat::Simulation>),
ObserveEvent(Document<observe::Event>),
ObserveDetection(Document<observe::Detection>),
ObserveProvider(Document<observe::Provider>),
ObserveConfiguration(Document<observe::Configuration>),
ReactStage(Document<react::Stage>),
ReactAction(Document<react::Action>),
ReactPlaybook(Document<react::Playbook>),
MitigateStrategy(Document<mitigate::Strategy>),
MitigatePlatform(Document<mitigate::Platform>),
MitigateConfiguration(Document<mitigate::Configuration>),
}
impl GenericDocument {
pub fn kind(&self) -> DomainModelKind {
match self {
Self::SourceIntelligence(_) => DomainModelKind::SourceIntelligence,
Self::SourceProvider(_) => DomainModelKind::SourceProvider,
Self::SourceRequirement(_) => DomainModelKind::SourceRequirement,
Self::ThreatTactic(_) => DomainModelKind::ThreatTactic,
Self::ThreatTechnique(_) => DomainModelKind::ThreatTechnique,
Self::ThreatSoftware(_) => DomainModelKind::ThreatSoftware,
Self::ThreatSimulation(_) => DomainModelKind::ThreatSimulation,
Self::ObserveEvent(_) => DomainModelKind::ObserveEvent,
Self::ObserveDetection(_) => DomainModelKind::ObserveDetection,
Self::ObserveProvider(_) => DomainModelKind::ObserveProvider,
Self::ObserveConfiguration(_) => DomainModelKind::ObserveConfiguration,
Self::ReactStage(_) => DomainModelKind::ReactStage,
Self::ReactAction(_) => DomainModelKind::ReactAction,
Self::ReactPlaybook(_) => DomainModelKind::ReactPlaybook,
Self::MitigateStrategy(_) => DomainModelKind::MitigateStrategy,
Self::MitigatePlatform(_) => DomainModelKind::MitigatePlatform,
Self::MitigateConfiguration(_) => DomainModelKind::MitigateConfiguration,
}
}
}

View File

@ -0,0 +1,75 @@
mod windows;
use serde::{Deserialize, Serialize};
use crate::domains::common::*;
pub use self::windows::WindowsEvent;
#[derive(Debug, Serialize, Deserialize)]
pub struct Event {
pub name: String,
#[serde(flatten)]
pub body: EventBody,
pub description: String,
}
impl DomainModel for Event {
fn kind() -> DomainModelKind {
DomainModelKind::ObserveEvent
}
fn name(&self) -> &str {
self.name.as_str()
}
}
#[derive(Debug, Serialize, Deserialize)]
pub enum EventBody {
WindowsEvent(WindowsEvent),
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Detection {
pub name: String,
}
impl DomainModel for Detection {
fn kind() -> DomainModelKind {
DomainModelKind::ObserveDetection
}
fn name(&self) -> &str {
self.name.as_str()
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Provider {
pub name: String,
}
impl DomainModel for Provider {
fn kind() -> DomainModelKind {
DomainModelKind::ObserveProvider
}
fn name(&self) -> &str {
self.name.as_str()
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Configuration {
pub name: String,
}
impl DomainModel for Configuration {
fn kind() -> DomainModelKind {
DomainModelKind::ObserveConfiguration
}
fn name(&self) -> &str {
self.name.as_str()
}
}

View File

@ -0,0 +1,71 @@
use serde::{Deserialize, Serialize};
use crate::util::xml;
#[derive(Debug, Serialize, Deserialize)]
pub struct WindowsEvent {
pub sample: Vec<Sample>,
}
#[derive(Debug)]
pub struct WindowsEventSummary<'a> {
pub event_id: u32,
pub channel: &'a str,
pub provider_name: &'a str,
pub provider_guid: Option<&'a str>,
}
impl WindowsEvent {
pub fn summary(&self) -> Result<WindowsEventSummary<'_>, &'static str> {
let sample = self.sample.get(0).ok_or("no sample")?;
let event_id: u32 = sample
.event_id()
.ok_or("no event id")?
.parse()
.map_err(|_| "event id not u32")?;
let (provider_name, provider_guid) = sample.provider();
Ok(WindowsEventSummary {
event_id,
channel: sample.channel().ok_or("no channel")?,
provider_name: provider_name.ok_or("no provider name")?,
provider_guid,
})
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Sample {
pub xml: xml::DocumentBuf,
pub description: Option<String>,
}
impl Sample {
pub fn provider_name(&self) -> Option<&str> {
self.provider().0
}
pub fn provider(&self) -> (Option<&str>, Option<&str>) {
self.get_event_element("System", "Provider")
.map(|p| (p.attribute("Name"), p.attribute("Guid")))
.unwrap_or_else(|| (None, None))
}
pub fn channel(&self) -> Option<&str> {
self.get_event_element("System", "Channel")
.and_then(|n| n.text())
}
pub fn event_id(&self) -> Option<&str> {
self.get_event_element("System", "EventID")
.and_then(|n| n.text())
}
fn get_event_element(&self, parent: &str, element: &str) -> Option<xml::Node> {
self.xml
.document()
.root_element()
.children()
.find(|c| c.tag_name().name() == parent)
.and_then(|c| c.children().find(|c| c.tag_name().name() == element))
}
}

71
src/domains/react.rs Normal file
View File

@ -0,0 +1,71 @@
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use crate::domains::common::*;
#[derive(Debug, Serialize, Deserialize)]
pub struct Stage {
pub name: String,
pub description: String,
#[serde(default)]
pub additional: Vec<(String, Value)>,
}
impl DomainModel for Stage {
fn kind() -> DomainModelKind {
DomainModelKind::ReactStage
}
fn name(&self) -> &str {
self.name.as_str()
}
}
///////////////////////////////////////////////////////////////////////////////
#[derive(Debug, Serialize, Deserialize)]
pub struct Action {
pub name: String,
pub description: String,
pub stage: DomainId,
#[serde(default)]
pub tags: Vec<String>,
#[serde(default)]
pub references: References,
#[serde(default)]
pub additional: Vec<(String, Value)>,
}
impl DomainModel for Action {
fn kind() -> DomainModelKind {
DomainModelKind::ReactAction
}
fn name(&self) -> &str {
self.name.as_str()
}
}
///////////////////////////////////////////////////////////////////////////////
#[derive(Debug, Serialize, Deserialize)]
pub struct Playbook {
pub name: String,
pub tags: Vec<String>,
pub description: String,
pub references: References,
pub stages: HashMap<DomainId, Vec<DomainId>>,
#[serde(default)]
pub additional: Vec<(String, Value)>,
}
impl DomainModel for Playbook {
fn kind() -> DomainModelKind {
DomainModelKind::ReactPlaybook
}
fn name(&self) -> &str {
self.name.as_str()
}
}

67
src/domains/source.rs Normal file
View File

@ -0,0 +1,67 @@
use serde::{Deserialize, Serialize};
use crate::domains::common::*;
#[derive(Debug, Serialize, Deserialize)]
pub struct Requirement {
pub name: String,
#[serde(default)]
pub intelligence: Vec<DomainId>,
#[serde(default)]
pub references: References,
#[serde(default)]
pub additional: Vec<(String, Value)>,
}
impl DomainModel for Requirement {
fn kind() -> DomainModelKind {
DomainModelKind::SourceRequirement
}
fn name(&self) -> &str {
self.name.as_str()
}
}
///////////////////////////////////////////////////////////////////////////////
#[derive(Debug, Serialize, Deserialize)]
pub struct Intelligence {
pub name: String,
pub provider: DomainId,
#[serde(default)]
pub references: References,
#[serde(default)]
pub additional: Vec<(String, Value)>,
}
impl DomainModel for Intelligence {
fn kind() -> DomainModelKind {
DomainModelKind::SourceIntelligence
}
fn name(&self) -> &str {
self.name.as_str()
}
}
///////////////////////////////////////////////////////////////////////////////
#[derive(Debug, Serialize, Deserialize)]
pub struct Provider {
pub name: String,
#[serde(default)]
pub references: References,
#[serde(default)]
pub additional: Vec<(String, Value)>,
}
impl DomainModel for Provider {
fn kind() -> DomainModelKind {
DomainModelKind::SourceProvider
}
fn name(&self) -> &str {
self.name.as_str()
}
}

63
src/domains/threat.rs Normal file
View File

@ -0,0 +1,63 @@
use serde::{Deserialize, Serialize};
use crate::domains::common::*;
#[derive(Debug, Serialize, Deserialize)]
pub struct Tactic {
pub name: String,
}
impl DomainModel for Tactic {
fn kind() -> DomainModelKind {
DomainModelKind::ThreatTactic
}
fn name(&self) -> &str {
self.name.as_str()
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Technique {
pub name: String,
}
impl DomainModel for Technique {
fn kind() -> DomainModelKind {
DomainModelKind::ThreatTechnique
}
fn name(&self) -> &str {
self.name.as_str()
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Software {
pub name: String,
}
impl DomainModel for Software {
fn kind() -> DomainModelKind {
DomainModelKind::ThreatSoftware
}
fn name(&self) -> &str {
self.name.as_str()
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Simulation {
pub name: String,
}
impl DomainModel for Simulation {
fn kind() -> DomainModelKind {
DomainModelKind::ThreatSimulation
}
fn name(&self) -> &str {
self.name.as_str()
}
}

357
src/generator/mdbook.rs Normal file
View File

@ -0,0 +1,357 @@
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::{io, mem};
use async_trait::async_trait;
use lazy_static::lazy_static;
use regex::Regex;
use serde::Serialize;
use tera::{Context, Map, Tera, Value};
use thiserror::Error;
use tokio::fs;
include!(concat!(env!("OUT_DIR"), "/mdbook-templates.rs"));
use crate::document::Document;
use crate::domains::common::{DomainId, DomainModel, ModelId};
use crate::domains::GenericDocument;
use crate::registry::{Registry, RegistryConfig};
use crate::generator::{Engine, GeneratorError};
#[derive(Debug, Error)]
pub enum MDBookEngineError {
#[error("{0}")]
Io(#[from] io::Error),
#[error("{0}")]
Tera(#[from] tera::Error),
}
impl From<MDBookEngineError> for GeneratorError {
fn from(err: MDBookEngineError) -> Self {
GeneratorError::Engine(err.into())
}
}
#[derive(Debug, Serialize)]
struct SummaryItem {
id: DomainId,
name: String,
}
pub struct MDBookEngine {
inner: Inner,
}
#[derive(Debug)]
#[allow(clippy::large_enum_variant)]
enum Inner {
Start {
src: PathBuf,
},
LoadAndRender {
src: PathBuf,
templates: Tera,