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,
|
||||||
|
summary: Vec<SummaryItem>,
|
||||||
|
documents: HashMap<DomainId, Value>,
|
||||||
|
},
|
||||||
|
Finish,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MDBookEngine {
|
||||||
|
pub fn new(src: &Path) -> Result<Self, MDBookEngineError> {
|
||||||
|
Ok(Self {
|
||||||
|
inner: Inner::Start {
|
||||||
|
src: src.canonicalize()?,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// (?Send)
|
||||||
|
#[async_trait]
|
||||||
|
impl Engine for MDBookEngine {
|
||||||
|
async fn start<R>(&mut self, _registry: &R) -> Result<(), GeneratorError>
|
||||||
|
where
|
||||||
|
R: Registry,
|
||||||
|
{
|
||||||
|
if let Inner::Start { src } = mem::replace(&mut self.inner, Inner::Finish) {
|
||||||
|
let templates = load_templates(&src).await?;
|
||||||
|
let documents = HashMap::new();
|
||||||
|
let summary = Vec::new();
|
||||||
|
self.inner = Inner::LoadAndRender {
|
||||||
|
src,
|
||||||
|
templates,
|
||||||
|
documents,
|
||||||
|
summary,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn push_document<R>(
|
||||||
|
&mut self,
|
||||||
|
model_id: ModelId,
|
||||||
|
doc: GenericDocument,
|
||||||
|
_registry: &R,
|
||||||
|
) -> Result<(), GeneratorError>
|
||||||
|
where
|
||||||
|
R: Registry,
|
||||||
|
{
|
||||||
|
if let Inner::LoadAndRender {
|
||||||
|
ref mut summary,
|
||||||
|
ref mut documents,
|
||||||
|
..
|
||||||
|
} = self.inner
|
||||||
|
{
|
||||||
|
macro_rules! push_documents {
|
||||||
|
($($kind:ident),+) => {
|
||||||
|
match doc {
|
||||||
|
$(
|
||||||
|
GenericDocument::$kind(doc) => {
|
||||||
|
save_domain_model(summary, documents, model_id, &doc);
|
||||||
|
}
|
||||||
|
)+
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
push_documents!(
|
||||||
|
SourceIntelligence,
|
||||||
|
SourceRequirement,
|
||||||
|
SourceProvider,
|
||||||
|
ThreatTactic,
|
||||||
|
ThreatTechnique,
|
||||||
|
ThreatSoftware,
|
||||||
|
ThreatSimulation,
|
||||||
|
ObserveEvent,
|
||||||
|
ObserveDetection,
|
||||||
|
ObserveProvider,
|
||||||
|
ObserveConfiguration,
|
||||||
|
ReactStage,
|
||||||
|
ReactAction,
|
||||||
|
ReactPlaybook,
|
||||||
|
MitigateStrategy,
|
||||||
|
MitigatePlatform,
|
||||||
|
MitigateConfiguration
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn finish<R>(&mut self, registry: &R) -> Result<(), GeneratorError>
|
||||||
|
where
|
||||||
|
R: Registry,
|
||||||
|
{
|
||||||
|
if let Inner::LoadAndRender {
|
||||||
|
src,
|
||||||
|
mut templates,
|
||||||
|
documents,
|
||||||
|
summary,
|
||||||
|
} = mem::replace(&mut self.inner, Inner::Finish)
|
||||||
|
{
|
||||||
|
let summary = Arc::new(summary);
|
||||||
|
let documents = Arc::new(documents);
|
||||||
|
let registry_config = registry.get_config().await?;
|
||||||
|
register_filters(&mut templates, documents.clone(), registry_config);
|
||||||
|
render_indexes(&templates, &src, &summary).await?;
|
||||||
|
render_summary(&templates, &src, &summary).await?;
|
||||||
|
render_documents(&templates, &src, documents).await?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn load_templates(src: &Path) -> Result<Tera, MDBookEngineError> {
|
||||||
|
let mut files = Vec::new();
|
||||||
|
|
||||||
|
for filename in BASE_TEMPLATES.file_names() {
|
||||||
|
let name = filename.strip_prefix("book/").unwrap();
|
||||||
|
let external = src.join(name);
|
||||||
|
|
||||||
|
let content = if external.is_file() {
|
||||||
|
fs::read_to_string(external).await?
|
||||||
|
} else {
|
||||||
|
let mut content = String::new();
|
||||||
|
BASE_TEMPLATES
|
||||||
|
.read(filename)?
|
||||||
|
.read_to_string(&mut content)?;
|
||||||
|
content
|
||||||
|
};
|
||||||
|
|
||||||
|
files.push((name, content));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut templates = Tera::default();
|
||||||
|
templates.add_raw_templates(files)?;
|
||||||
|
Ok(templates)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_domain_model<M>(
|
||||||
|
summary: &mut Vec<SummaryItem>,
|
||||||
|
documents: &mut HashMap<DomainId, Value>,
|
||||||
|
model_id: ModelId,
|
||||||
|
doc: &Document<M>,
|
||||||
|
) where
|
||||||
|
M: DomainModel,
|
||||||
|
{
|
||||||
|
let id = DomainId::new(M::kind(), model_id);
|
||||||
|
let mut value = Map::new();
|
||||||
|
value.insert("id".to_owned(), tera::to_value(&id).unwrap());
|
||||||
|
value.insert("doc".to_owned(), tera::to_value(&doc).unwrap());
|
||||||
|
documents.insert(id, value.into());
|
||||||
|
summary.push(SummaryItem {
|
||||||
|
id,
|
||||||
|
name: doc.meta.name().to_owned(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn render_documents(
|
||||||
|
templates: &Tera,
|
||||||
|
src: &Path,
|
||||||
|
documents: Arc<HashMap<DomainId, Value>>,
|
||||||
|
) -> Result<(), MDBookEngineError> {
|
||||||
|
for (id, value) in documents.iter() {
|
||||||
|
let (domain, model) = id.kind.parts();
|
||||||
|
let model_path = format!("{}/{}", domain, model);
|
||||||
|
let template_path = format!("{}.instance.tera", model_path);
|
||||||
|
let output_path = src
|
||||||
|
.join(model_path)
|
||||||
|
.join(id.model_id.to_string())
|
||||||
|
.with_extension("md");
|
||||||
|
let context = Context::from_value(value.clone())?;
|
||||||
|
let contents = templates.render(&template_path, &context)?;
|
||||||
|
fs::write(output_path, contents).await?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn render_summary(
|
||||||
|
templates: &Tera,
|
||||||
|
src: &Path,
|
||||||
|
summary: &[SummaryItem],
|
||||||
|
) -> Result<(), MDBookEngineError> {
|
||||||
|
let mut context = Context::new();
|
||||||
|
context.insert("summary", summary);
|
||||||
|
|
||||||
|
let contents = templates.render("summary.tera", &context)?;
|
||||||
|
let summary_path = src.join("SUMMARY.md");
|
||||||
|
fs::write(summary_path, contents).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn render_indexes(
|
||||||
|
templates: &Tera,
|
||||||
|
src: &Path,
|
||||||
|
summary: &[SummaryItem],
|
||||||
|
) -> Result<(), MDBookEngineError> {
|
||||||
|
let mut last_i = 0;
|
||||||
|
let mut last_kind = None;
|
||||||
|
for (i, item) in summary.iter().enumerate() {
|
||||||
|
if Some(item.id.kind) != last_kind {
|
||||||
|
let (domain, model) = item.id.kind.parts();
|
||||||
|
|
||||||
|
let domain_dir = src.join(domain);
|
||||||
|
let model_dir = domain_dir.join(model);
|
||||||
|
|
||||||
|
let mut context = Context::new();
|
||||||
|
context.insert("instances", &summary[last_i..i]);
|
||||||
|
|
||||||
|
if !domain_dir.is_dir() {
|
||||||
|
fs::create_dir(&domain_dir).await?;
|
||||||
|
}
|
||||||
|
if !model_dir.is_dir() {
|
||||||
|
fs::create_dir(&model_dir).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let model_index = model_dir.with_extension("md");
|
||||||
|
let template_path = format!("{}/{}.tera", domain, model);
|
||||||
|
let contents = templates.render(&template_path, &context)?;
|
||||||
|
fs::write(model_index, contents).await?;
|
||||||
|
|
||||||
|
last_i = i;
|
||||||
|
last_kind = Some(item.id.kind);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn register_filters(
|
||||||
|
tera: &mut Tera,
|
||||||
|
registry: Arc<HashMap<DomainId, Value>>,
|
||||||
|
registry_config: Arc<RegistryConfig>,
|
||||||
|
) {
|
||||||
|
use tera::{try_get_value, Error};
|
||||||
|
|
||||||
|
fn parse_domain_id(value: &Value) -> Result<DomainId, Error> {
|
||||||
|
let id = try_get_value!("link", "value", String, value);
|
||||||
|
id.parse()
|
||||||
|
.map_err(|_| Error::msg(format_args!("failed to parse domain id `{}`", id)))
|
||||||
|
}
|
||||||
|
|
||||||
|
tera.register_filter(
|
||||||
|
"domain_id_link",
|
||||||
|
move |value: &Value, args: &HashMap<String, Value>| {
|
||||||
|
let id = parse_domain_id(value)?;
|
||||||
|
let (domain, model) = id.kind.parts();
|
||||||
|
let link = match args.get("for").and_then(|v| v.as_str()) {
|
||||||
|
None => format!("/{}/{}/{}.md", domain, model, id.model_id),
|
||||||
|
Some("model") => format!("/{}/{}.md", domain, model),
|
||||||
|
Some("edit") => registry_config.edit_link(id).map_err(Error::msg)?,
|
||||||
|
Some(_) => {
|
||||||
|
return Err(Error::msg(
|
||||||
|
"domain_id_link `for` arg can only be `model|edit`",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(Value::String(link))
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
tera.register_filter(
|
||||||
|
"domain_id",
|
||||||
|
move |value: &Value, _: &HashMap<String, Value>| {
|
||||||
|
let id = parse_domain_id(value)?;
|
||||||
|
let (domain, model) = id.kind.parts();
|
||||||
|
let mut parts = Map::with_capacity(4);
|
||||||
|
parts.insert("domain".to_owned(), domain.into());
|
||||||
|
parts.insert("model".to_owned(), model.into());
|
||||||
|
parts.insert("model_id".to_owned(), id.model_id.to_string().into());
|
||||||
|
Ok(Value::Object(parts))
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
tera.register_filter(
|
||||||
|
"get_doc",
|
||||||
|
move |value: &Value, _: &HashMap<String, Value>| {
|
||||||
|
let id = parse_domain_id(value)?;
|
||||||
|
registry
|
||||||
|
.get(&id)
|
||||||
|
.cloned()
|
||||||
|
.ok_or_else(|| Error::msg(format_args!("unknown document id {}", id)))
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
tera.register_filter("autolink", |value: &Value, _: &HashMap<String, Value>| {
|
||||||
|
let text = try_get_value!("autolink", "value", String, value);
|
||||||
|
|
||||||
|
if text.is_empty() {
|
||||||
|
return Ok(Value::String(String::new()));
|
||||||
|
}
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
static ref URL: Regex = Regex::new(
|
||||||
|
r"(?ix)
|
||||||
|
\b(([\w-]+://?|www[.])[^\s()<>]+(?:\([\w\d]+\)|([^[:punct:]\s]|/)))
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let replaced = URL.replace_all(text.as_str(), "[$0]($0)").into_owned();
|
||||||
|
|
||||||
|
Ok(Value::String(replaced))
|
||||||
|
});
|
||||||
|
}
|
101
src/generator/mod.rs
Normal file
101
src/generator/mod.rs
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
pub mod mdbook;
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use crate::document::Document;
|
||||||
|
use crate::domains::common::{DomainId, DomainModel, ModelId};
|
||||||
|
use crate::domains::GenericDocument;
|
||||||
|
use crate::registry::{Registry, RegistryError};
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum GeneratorError {
|
||||||
|
#[error("{0}")]
|
||||||
|
Engine(#[from] anyhow::Error),
|
||||||
|
#[error("{0}")]
|
||||||
|
Registry(#[from] RegistryError),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Generator<E, R> {
|
||||||
|
engine: E,
|
||||||
|
registry: R,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E, R> Generator<E, R>
|
||||||
|
where
|
||||||
|
E: Engine,
|
||||||
|
R: Registry,
|
||||||
|
{
|
||||||
|
pub fn new(engine: E, registry: R) -> Self {
|
||||||
|
Self { engine, registry }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn generate(mut self) -> Result<(), GeneratorError> {
|
||||||
|
self.engine.start(&self.registry).await?;
|
||||||
|
macro_rules! push_documents {
|
||||||
|
($($kind:ident),+) => {
|
||||||
|
$(
|
||||||
|
for (id, doc) in self.get_documents().await? {
|
||||||
|
self.engine
|
||||||
|
.push_document(id.model_id, GenericDocument::$kind(doc), &self.registry)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
)*
|
||||||
|
};
|
||||||
|
}
|
||||||
|
push_documents!(
|
||||||
|
SourceIntelligence,
|
||||||
|
SourceRequirement,
|
||||||
|
SourceProvider,
|
||||||
|
ThreatTactic,
|
||||||
|
ThreatTechnique,
|
||||||
|
ThreatSoftware,
|
||||||
|
ThreatSimulation,
|
||||||
|
ObserveEvent,
|
||||||
|
ObserveDetection,
|
||||||
|
ObserveProvider,
|
||||||
|
ObserveConfiguration,
|
||||||
|
ReactStage,
|
||||||
|
ReactAction,
|
||||||
|
ReactPlaybook,
|
||||||
|
MitigateStrategy,
|
||||||
|
MitigatePlatform,
|
||||||
|
MitigateConfiguration
|
||||||
|
);
|
||||||
|
self.engine.finish(&self.registry).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_documents<M>(&self) -> Result<Vec<(DomainId, Document<M>)>, GeneratorError>
|
||||||
|
where
|
||||||
|
M: DomainModel,
|
||||||
|
{
|
||||||
|
self.registry
|
||||||
|
.get_documents()
|
||||||
|
.await
|
||||||
|
.map(|mut docs| {
|
||||||
|
docs.sort_by_key(|(id, _)| *id);
|
||||||
|
docs
|
||||||
|
})
|
||||||
|
.map_err(GeneratorError::Registry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait Engine {
|
||||||
|
async fn start<R>(&mut self, registry: &R) -> Result<(), GeneratorError>
|
||||||
|
where
|
||||||
|
R: Registry;
|
||||||
|
|
||||||
|
async fn push_document<R>(
|
||||||
|
&mut self,
|
||||||
|
id: ModelId,
|
||||||
|
doc: GenericDocument,
|
||||||
|
registry: &R,
|
||||||
|
) -> Result<(), GeneratorError>
|
||||||
|
where
|
||||||
|
R: Registry;
|
||||||
|
|
||||||
|
async fn finish<R>(&mut self, registry: &R) -> Result<(), GeneratorError>
|
||||||
|
where
|
||||||
|
R: Registry;
|
||||||
|
}
|
6
src/lib.rs
Normal file
6
src/lib.rs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
mod util;
|
||||||
|
|
||||||
|
pub mod document;
|
||||||
|
pub mod domains;
|
||||||
|
pub mod generator;
|
||||||
|
pub mod registry;
|
72
src/main.rs
Normal file
72
src/main.rs
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use anyhow::{Context, Error};
|
||||||
|
use structopt::StructOpt;
|
||||||
|
|
||||||
|
use cyberstorm::generator::mdbook::MDBookEngine;
|
||||||
|
use cyberstorm::generator::Generator;
|
||||||
|
use cyberstorm::registry::{DirectoryRegistry, SupportedRegistry};
|
||||||
|
|
||||||
|
#[derive(Debug, StructOpt)]
|
||||||
|
#[structopt(name = "cyberstorm")]
|
||||||
|
struct Opt {
|
||||||
|
/// Registry to use.
|
||||||
|
registry: RegistryOpt,
|
||||||
|
|
||||||
|
#[structopt(subcommand)]
|
||||||
|
cmd: Cmd,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, StructOpt)]
|
||||||
|
enum Cmd {
|
||||||
|
BuildMdbook(BuildMdbookOpt),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, StructOpt)]
|
||||||
|
struct BuildMdbookOpt {
|
||||||
|
/// Path to the output MDBook directory.
|
||||||
|
#[structopt(parse(from_os_str))]
|
||||||
|
book: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum RegistryOpt {
|
||||||
|
Directory(PathBuf),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for RegistryOpt {
|
||||||
|
type Err = &'static str;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
Ok(Self::Directory(PathBuf::from(s)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run(opt: Opt) -> Result<(), Error> {
|
||||||
|
let registry = match &opt.registry {
|
||||||
|
RegistryOpt::Directory(path) => SupportedRegistry::Directory(DirectoryRegistry::new(path)),
|
||||||
|
};
|
||||||
|
match &opt.cmd {
|
||||||
|
Cmd::BuildMdbook(build_opt) => build_mdbook(&opt, build_opt, ®istry).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn build_mdbook(
|
||||||
|
_opt: &Opt,
|
||||||
|
build_opt: &BuildMdbookOpt,
|
||||||
|
registry: &SupportedRegistry,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let engine = MDBookEngine::new(&build_opt.book).context("failed to load mdbook engine")?;
|
||||||
|
Generator::new(engine, registry)
|
||||||
|
.generate()
|
||||||
|
.await
|
||||||
|
.context("failed to generate mdbook content")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
if let Err(err) = run(Opt::from_args()).await {
|
||||||
|
eprintln!("{:#}", err);
|
||||||
|
}
|
||||||
|
}
|
105
src/registry/directory.rs
Normal file
105
src/registry/directory.rs
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use anyhow::Context;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use tokio::fs;
|
||||||
|
|
||||||
|
use crate::document::Document;
|
||||||
|
use crate::domains::common::*;
|
||||||
|
use crate::registry::{Registry, RegistryConfig, RegistryError};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct DirectoryRegistry {
|
||||||
|
path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DirectoryRegistry {
|
||||||
|
pub fn new<P: Into<PathBuf>>(path: P) -> Self {
|
||||||
|
Self { path: path.into() }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn domain_model_kind_path(&self, kind: DomainModelKind) -> PathBuf {
|
||||||
|
let (domain, model) = kind.parts();
|
||||||
|
self.path.join(format!("{}/{}", domain, model))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn domain_id_path(&self, id: DomainId) -> PathBuf {
|
||||||
|
let (domain, model) = id.kind.parts();
|
||||||
|
self.path
|
||||||
|
.join(format!("{}/{}/{}.md", domain, model, id.model_id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Registry for DirectoryRegistry {
|
||||||
|
async fn get_config(&self) -> Result<Arc<RegistryConfig>, RegistryError> {
|
||||||
|
let config_str = fs::read_to_string(self.path.join("config.toml")).await?;
|
||||||
|
toml::from_str(&config_str)
|
||||||
|
.context("failed to read registry `config.toml`")
|
||||||
|
.map(Arc::new)
|
||||||
|
.map_err(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_document<M>(&self, model_id: ModelId) -> Result<Document<M>, RegistryError>
|
||||||
|
where
|
||||||
|
M: DomainModel,
|
||||||
|
{
|
||||||
|
let id = DomainId::new(M::kind(), model_id);
|
||||||
|
let path = self.domain_id_path(id);
|
||||||
|
if !path.is_file() {
|
||||||
|
return Err(RegistryError::NotFound(id));
|
||||||
|
}
|
||||||
|
let s = fs::read_to_string(path).await?;
|
||||||
|
s.parse().map_err(|err| RegistryError::Document(id, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_documents<M>(&self) -> Result<Vec<(DomainId, Document<M>)>, RegistryError>
|
||||||
|
where
|
||||||
|
M: DomainModel,
|
||||||
|
{
|
||||||
|
let ids = self.get_document_ids(M::kind()).await?;
|
||||||
|
let mut docs = Vec::with_capacity(ids.len());
|
||||||
|
for id in ids {
|
||||||
|
docs.push((id, self.get_document(id.model_id).await?));
|
||||||
|
}
|
||||||
|
Ok(docs)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_document_ids(
|
||||||
|
&self,
|
||||||
|
kind: DomainModelKind,
|
||||||
|
) -> Result<Vec<DomainId>, RegistryError> {
|
||||||
|
let mut ids = Vec::new();
|
||||||
|
let path = self.domain_model_kind_path(kind);
|
||||||
|
if !path.is_dir() {
|
||||||
|
return Ok(vec![]);
|
||||||
|
}
|
||||||
|
let mut dirs = fs::read_dir(path).await?;
|
||||||
|
while let Some(entry) = dirs.next_entry().await? {
|
||||||
|
let path = entry.path();
|
||||||
|
if path.is_file() {
|
||||||
|
let filestem = path.file_stem().unwrap().to_string_lossy();
|
||||||
|
let model_id: ModelId = filestem.as_ref().parse().unwrap();
|
||||||
|
if !model_id.is_example() {
|
||||||
|
ids.push(DomainId::new(kind, model_id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(ids)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn put_document<M>(
|
||||||
|
&self,
|
||||||
|
model_id: ModelId,
|
||||||
|
document: Document<M>,
|
||||||
|
) -> Result<(), RegistryError>
|
||||||
|
where
|
||||||
|
M: DomainModel,
|
||||||
|
{
|
||||||
|
let path = self.domain_id_path(DomainId::new(M::kind(), model_id));
|
||||||
|
let document = document.to_string();
|
||||||
|
fs::write(path, document.as_str()).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
178
src/registry/mod.rs
Normal file
178
src/registry/mod.rs
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
mod directory;
|
||||||
|
|
||||||
|
use std::fmt::Debug;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use anyhow::{anyhow, Context as _};
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tera::{Context, Tera};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use crate::document::{Document, MetaError};
|
||||||
|
use crate::domains::common::*;
|
||||||
|
|
||||||
|
pub use self::directory::DirectoryRegistry;
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum RegistryError {
|
||||||
|
#[error("config error: {0}")]
|
||||||
|
Config(anyhow::Error),
|
||||||
|
#[error("failed to parse document {0}: {1}")]
|
||||||
|
Document(DomainId, MetaError),
|
||||||
|
#[error("domain id {0} not found")]
|
||||||
|
NotFound(DomainId),
|
||||||
|
#[error("{0}")]
|
||||||
|
Other(#[from] anyhow::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<std::io::Error> for RegistryError {
|
||||||
|
fn from(err: std::io::Error) -> Self {
|
||||||
|
Self::Other(err.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
pub struct RegistryConfig {
|
||||||
|
edit_link: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RegistryConfig {
|
||||||
|
pub fn edit_link(&self, domain_id: DomainId) -> Result<String, RegistryError> {
|
||||||
|
let edit_link = self
|
||||||
|
.edit_link
|
||||||
|
.as_deref()
|
||||||
|
.ok_or_else(|| RegistryError::Config(anyhow!("edit-link option not set")))?;
|
||||||
|
let (domain, model) = domain_id.kind.parts();
|
||||||
|
let mut context = Context::new();
|
||||||
|
context.insert("id", &domain_id);
|
||||||
|
context.insert("domain", domain);
|
||||||
|
context.insert("model", model);
|
||||||
|
context.insert("model_id", &domain_id.model_id);
|
||||||
|
|
||||||
|
Tera::one_off(edit_link, &context, false)
|
||||||
|
.context("error rendering edit link")
|
||||||
|
.map_err(RegistryError::Config)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait Registry: Send + Sync {
|
||||||
|
async fn get_config(&self) -> Result<Arc<RegistryConfig>, RegistryError>;
|
||||||
|
|
||||||
|
async fn get_document<M>(&self, model_id: ModelId) -> Result<Document<M>, RegistryError>
|
||||||
|
where
|
||||||
|
M: DomainModel;
|
||||||
|
|
||||||
|
async fn get_documents<M>(&self) -> Result<Vec<(DomainId, Document<M>)>, RegistryError>
|
||||||
|
where
|
||||||
|
M: DomainModel;
|
||||||
|
|
||||||
|
async fn get_document_ids(&self, kind: DomainModelKind)
|
||||||
|
-> Result<Vec<DomainId>, RegistryError>;
|
||||||
|
|
||||||
|
async fn put_document<M>(
|
||||||
|
&self,
|
||||||
|
model_id: ModelId,
|
||||||
|
document: Document<M>,
|
||||||
|
) -> Result<(), RegistryError>
|
||||||
|
where
|
||||||
|
M: DomainModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<T> Registry for &T
|
||||||
|
where
|
||||||
|
T: Registry,
|
||||||
|
{
|
||||||
|
async fn get_config(&self) -> Result<Arc<RegistryConfig>, RegistryError> {
|
||||||
|
(**self).get_config().await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_document<M>(&self, model_id: ModelId) -> Result<Document<M>, RegistryError>
|
||||||
|
where
|
||||||
|
M: DomainModel,
|
||||||
|
{
|
||||||
|
(**self).get_document(model_id).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_documents<M>(&self) -> Result<Vec<(DomainId, Document<M>)>, RegistryError>
|
||||||
|
where
|
||||||
|
M: DomainModel,
|
||||||
|
{
|
||||||
|
(**self).get_documents().await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_document_ids(
|
||||||
|
&self,
|
||||||
|
kind: DomainModelKind,
|
||||||
|
) -> Result<Vec<DomainId>, RegistryError> {
|
||||||
|
(**self).get_document_ids(kind).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn put_document<M>(
|
||||||
|
&self,
|
||||||
|
model_id: ModelId,
|
||||||
|
document: Document<M>,
|
||||||
|
) -> Result<(), RegistryError>
|
||||||
|
where
|
||||||
|
M: DomainModel,
|
||||||
|
{
|
||||||
|
(**self).put_document(model_id, document).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum SupportedRegistry {
|
||||||
|
Directory(DirectoryRegistry),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Registry for SupportedRegistry {
|
||||||
|
async fn get_config(&self) -> Result<Arc<RegistryConfig>, RegistryError> {
|
||||||
|
match self {
|
||||||
|
Self::Directory(r) => r.get_config().await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_document<M>(&self, model_id: ModelId) -> Result<Document<M>, RegistryError>
|
||||||
|
where
|
||||||
|
M: DomainModel,
|
||||||
|
{
|
||||||
|
match self {
|
||||||
|
Self::Directory(r) => r.get_document(model_id).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_documents<M>(&self) -> Result<Vec<(DomainId, Document<M>)>, RegistryError>
|
||||||
|
where
|
||||||
|
M: DomainModel,
|
||||||
|
{
|
||||||
|
match self {
|
||||||
|
Self::Directory(r) => r.get_documents().await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_document_ids(
|
||||||
|
&self,
|
||||||
|
kind: DomainModelKind,
|
||||||
|
) -> Result<Vec<DomainId>, RegistryError> {
|
||||||
|
match self {
|
||||||
|
Self::Directory(r) => r.get_document_ids(kind).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn put_document<M>(
|
||||||
|
&self,
|
||||||
|
model_id: ModelId,
|
||||||
|
document: Document<M>,
|
||||||
|
) -> Result<(), RegistryError>
|
||||||
|
where
|
||||||
|
M: DomainModel,
|
||||||
|
{
|
||||||
|
match self {
|
||||||
|
Self::Directory(r) => r.put_document(model_id, document).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1
src/util/mod.rs
Normal file
1
src/util/mod.rs
Normal file
@ -0,0 +1 @@
|
|||||||
|
pub mod xml;
|
64
src/util/xml.rs
Normal file
64
src/util/xml.rs
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
use core::convert::TryFrom;
|
||||||
|
use core::fmt;
|
||||||
|
|
||||||
|
use roxmltree::Document;
|
||||||
|
use zc::{Dependant, Zc};
|
||||||
|
|
||||||
|
pub use roxmltree::{self, *};
|
||||||
|
|
||||||
|
pub struct DocumentBuf(Zc<String, DependantDocument<'static>>);
|
||||||
|
|
||||||
|
impl DocumentBuf {
|
||||||
|
pub fn document<'input>(&'input self) -> &Document<'input> {
|
||||||
|
&self.0.get::<DependantDocument<'input>>().0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for DocumentBuf {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
fmt::Debug::fmt(self.document(), f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<String> for DocumentBuf {
|
||||||
|
type Error = roxmltree::Error;
|
||||||
|
|
||||||
|
fn try_from(s: String) -> Result<Self, Self::Error> {
|
||||||
|
match zc::try_from!(s, DependantDocument, str) {
|
||||||
|
Ok(doc) => Ok(Self(doc)),
|
||||||
|
Err((err, _)) => Err(err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl serde::ser::Serialize for DocumentBuf {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::ser::Serializer,
|
||||||
|
{
|
||||||
|
serializer.serialize_str(self.0.as_owned())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> serde::de::Deserialize<'de> for DocumentBuf {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::de::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
use serde::de::Error;
|
||||||
|
Self::try_from(String::deserialize(deserializer)?).map_err(D::Error::custom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
struct DependantDocument<'input>(Document<'input>);
|
||||||
|
|
||||||
|
unsafe impl<'o> Dependant<'o> for DependantDocument<'o> {
|
||||||
|
type Static = DependantDocument<'static>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'input> TryFrom<&'input str> for DependantDocument<'input> {
|
||||||
|
type Error = roxmltree::Error;
|
||||||
|
|
||||||
|
fn try_from(s: &'input str) -> Result<Self, Self::Error> {
|
||||||
|
Document::parse(s).map(Self)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user