initial commit
This commit is contained in:
commit
22a4e9fbf8
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
/target
|
||||
Cargo.lock
|
||||
|
||||
# STORM
|
||||
/dist
|
||||
|
29
Cargo.toml
Normal file
29
Cargo.toml
Normal 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
84
README.md
Normal 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
14
book.toml
Normal 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
6
book/.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
/SUMMARY.md
|
||||
/source/**/*.md
|
||||
/threat/**/*.md
|
||||
/observe/**/*.md
|
||||
/react/**/*.md
|
||||
/mitigate/**/*.md
|
49
book/macros.tera
Normal file
49
book/macros.tera
Normal 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 %}
|
22
book/observe/event.instance.tera
Normal file
22
book/observe/event.instance.tera
Normal 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
7
book/observe/event.tera
Normal 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) }}
|
17
book/react/action.instance.tera
Normal file
17
book/react/action.instance.tera
Normal 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
5
book/react/action.tera
Normal file
@ -0,0 +1,5 @@
|
||||
{% import "macros.tera" as macros %}
|
||||
|
||||
# Action
|
||||
|
||||
{{ macros::summary_list(instances=instances) }}
|
10
book/react/stage.instance.tera
Normal file
10
book/react/stage.instance.tera
Normal 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
7
book/react/stage.tera
Normal file
@ -0,0 +1,7 @@
|
||||
{% import "macros.tera" as macros %}
|
||||
|
||||
# Stage
|
||||
|
||||
Phase of a response to observed threat behaviour.
|
||||
|
||||
{{ macros::summary_list(instances=instances) }}
|
14
book/source/intelligence.instance.tera
Normal file
14
book/source/intelligence.instance.tera
Normal 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) }}
|
7
book/source/intelligence.tera
Normal file
7
book/source/intelligence.tera
Normal 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) }}
|
13
book/source/provider.instance.tera
Normal file
13
book/source/provider.instance.tera
Normal 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) }}
|
7
book/source/provider.tera
Normal file
7
book/source/provider.tera
Normal file
@ -0,0 +1,7 @@
|
||||
{% import "macros.tera" as macros %}
|
||||
|
||||
# Provider
|
||||
|
||||
An internal or external supplier of intelligence.
|
||||
|
||||
{{ macros::summary_list(instances=instances) }}
|
13
book/source/requirement.instance.tera
Normal file
13
book/source/requirement.instance.tera
Normal 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) }}
|
7
book/source/requirement.tera
Normal file
7
book/source/requirement.tera
Normal 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
16
book/summary.tera
Normal 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
13
build.rs
Normal 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
1
registry/config.toml
Normal file
@ -0,0 +1 @@
|
||||
edit-link = "https://localhost/registry/{{domain}}/{{model}}/{{model_id}}.md"
|
3
registry/mitigate/configuration/00000.md
Normal file
3
registry/mitigate/configuration/00000.md
Normal file
@ -0,0 +1,3 @@
|
||||
+++
|
||||
|
||||
+++
|
3
registry/mitigate/platform/00000.md
Normal file
3
registry/mitigate/platform/00000.md
Normal file
@ -0,0 +1,3 @@
|
||||
+++
|
||||
|
||||
+++
|
3
registry/mitigate/strategy/00000.md
Normal file
3
registry/mitigate/strategy/00000.md
Normal file
@ -0,0 +1,3 @@
|
||||
+++
|
||||
|
||||
+++
|
3
registry/observe/configuration/00000.md
Normal file
3
registry/observe/configuration/00000.md
Normal file
@ -0,0 +1,3 @@
|
||||
+++
|
||||
|
||||
+++
|
3
registry/observe/detection/00000.md
Normal file
3
registry/observe/detection/00000.md
Normal file
@ -0,0 +1,3 @@
|
||||
+++
|
||||
|
||||
+++
|
43
registry/observe/event/00000.md
Normal file
43
registry/observe/event/00000.md
Normal 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
|
3
registry/observe/provider/00000.md
Normal file
3
registry/observe/provider/00000.md
Normal file
@ -0,0 +1,3 @@
|
||||
+++
|
||||
|
||||
+++
|
6
registry/react/action/00001.md
Normal file
6
registry/react/action/00001.md
Normal 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'
|
||||
+++
|
||||
|
4
registry/react/stage/00001.md
Normal file
4
registry/react/stage/00001.md
Normal file
@ -0,0 +1,4 @@
|
||||
+++
|
||||
name = "Preparation"
|
||||
description = "Get prepared for a security incident"
|
||||
+++
|
4
registry/react/stage/00002.md
Normal file
4
registry/react/stage/00002.md
Normal file
@ -0,0 +1,4 @@
|
||||
+++
|
||||
name = "Identification"
|
||||
description = "Gather information about a threat that has triggered a security incident, its TTPs, and affected assets"
|
||||
+++
|
4
registry/react/stage/00003.md
Normal file
4
registry/react/stage/00003.md
Normal file
@ -0,0 +1,4 @@
|
||||
+++
|
||||
name = "Containment"
|
||||
description = "Prevent a threat from achieving its objectives and/or spreading around an environment"
|
||||
+++
|
4
registry/react/stage/00004.md
Normal file
4
registry/react/stage/00004.md
Normal file
@ -0,0 +1,4 @@
|
||||
+++
|
||||
name = "Eradication"
|
||||
description = "Remove a threat from an environment"
|
||||
+++
|
4
registry/react/stage/00005.md
Normal file
4
registry/react/stage/00005.md
Normal file
@ -0,0 +1,4 @@
|
||||
+++
|
||||
name = "Recovery"
|
||||
description = "Recover from the incident and return all the assets back to normal operation"
|
||||
+++
|
4
registry/react/stage/00006.md
Normal file
4
registry/react/stage/00006.md
Normal file
@ -0,0 +1,4 @@
|
||||
+++
|
||||
name = "Lessons Learned"
|
||||
description = "Discover how to improve the Incident Response process and implement the improvements"
|
||||
+++
|
7
registry/source/intelligence/00001.md
Normal file
7
registry/source/intelligence/00001.md
Normal file
@ -0,0 +1,7 @@
|
||||
+++
|
||||
name = "DomainTools Whois Lookup"
|
||||
provider = "SPR0001"
|
||||
references = [
|
||||
"https://whois.domaintools.com/"
|
||||
]
|
||||
+++
|
3
registry/source/provider/00001.md
Normal file
3
registry/source/provider/00001.md
Normal file
@ -0,0 +1,3 @@
|
||||
+++
|
||||
name = "DomainTools"
|
||||
+++
|
3
registry/source/provider/00002.md
Normal file
3
registry/source/provider/00002.md
Normal file
@ -0,0 +1,3 @@
|
||||
+++
|
||||
name = "VirusTotal"
|
||||
+++
|
6
registry/source/requirement/00001.md
Normal file
6
registry/source/requirement/00001.md
Normal file
@ -0,0 +1,6 @@
|
||||
+++
|
||||
name = "OSINT assessment of IPv4/IPv6 address"
|
||||
intelligence = [
|
||||
"SIE0001",
|
||||
]
|
||||
+++
|
6
registry/source/requirement/00002.md
Normal file
6
registry/source/requirement/00002.md
Normal file
@ -0,0 +1,6 @@
|
||||
+++
|
||||
name = "OSINT assessment of domain"
|
||||
intelligence = [
|
||||
"SIE0001",
|
||||
]
|
||||
+++
|
3
registry/source/requirement/00003.md
Normal file
3
registry/source/requirement/00003.md
Normal file
@ -0,0 +1,3 @@
|
||||
+++
|
||||
name = "OSINT assessment of MD5 file hash"
|
||||
+++
|
3
registry/source/requirement/00004.md
Normal file
3
registry/source/requirement/00004.md
Normal file
@ -0,0 +1,3 @@
|
||||
+++
|
||||
name = "OSINT assessment of SHA256 file hash"
|
||||
+++
|
73
src/document.rs
Normal file
73
src/document.rs
Normal 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
236
src/domains/common.rs
Normal 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
48
src/domains/mitigate.rs
Normal 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
54
src/domains/mod.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
75
src/domains/observe/mod.rs
Normal file
75
src/domains/observe/mod.rs
Normal 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()
|
||||
}
|
||||
}
|
71
src/domains/observe/windows.rs
Normal file
71
src/domains/observe/windows.rs
Normal 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
71
src/domains/react.rs
Normal 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
67
src/domains/source.rs
Normal 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
63
src/domains/threat.rs
Normal 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
357
src/generator/mdbook.rs
Normal 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,
|
||||
|