Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: a storm stix import config builder + a custom config #37

Draft
wants to merge 17 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
recursive-include src/stormlibpp/*/storm/ *.storm README.md
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ change-version: # Change the version of this project (requires VERSION=#.#.#)
.PHONY: build
build: # Build the package tarball and wheel
pipenv run python3 -m build .
pipenv run python3 scripts/build_stix_conf.py

.PHONY: setup
setup: # Setup this project's pipenv environment
Expand Down
167 changes: 167 additions & 0 deletions data/stix_import_conf.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
{
"addbundle": true,
"bundle": {
"storm": "it:sec:stix:bundle:id=$bundle.id\nreturn($node)\n[ it:sec:stix:bundle=* :id=$bundle.id ]\n$node.data.set(stix:bundle, $object)\nreturn($node)"
},
"objects": {
"artifact": {
"storm": "// TODO - Figure out what these look like."
},
"attack-pattern": {
"storm": "// TODO - Figure out what these look like."
},
"autonomous-system": {
"storm": "// TODO - Make sure value is the right key for this\n[inet:as=$object.value]\n[ <(refs)+ { it:sec:stix:bundle:id=$bundle.id } ]\n$node.data.set(stix:object, $object)\nreturn($node)"
},
"campaign": {
"storm": "[ ou:campaign=(stix, campaign, $object.id)\n :name?=$object.name\n :desc?=$object.description\n .seen?=$object.last_seen\n .seen?=$object.first_seen\n]\n[ <(refs)+ { it:sec:stix:bundle:id=$bundle.id } ]\n$node.data.set(stix:object, $object)\nreturn($node)"
},
"course-of-action": {
"storm": "[ risk:mitigation=(stix, course-of-action, $object.id)\n :name?=$object.name\n :desc?=$object.description\n]\n[ <(refs)+ { it:sec:stix:bundle:id=$bundle.id } ]\n$node.data.set(stix:object, $object)\nreturn($node)"
},
"directory": {
"storm": "// TODO - Figure out what these look like."
},
"domain-name": {
"storm": "// TODO - Make sure value is the right key for this\n[inet:fqdn=$object.value]\n[ <(refs)+ { it:sec:stix:bundle:id=$bundle.id } ]\n$node.data.set(stix:object, $object)\nreturn($node)"
},
"email-addr": {
"storm": "// TODO - Make sure value is the right key for this\n[inet:email=$object.value]\n[ <(refs)+ { it:sec:stix:bundle:id=$bundle.id } ]\n$node.data.set(stix:object, $object)\nreturn($node)"
},
"email-message": {
"storm": "// TODO - Figure out what these look like."
},
"file": {
"storm": "if ($object.hashes != $lib.null) {\n ($ok, $hash) = $lib.trycast(file:bytes, $object.hashes.\"SHA-256\")\n if $ok {\n $guid = `sha256:{$hash}`\n } else {\n $guid = `guid:{$lib.guid(stix, file, $object.id)}`\n }\n [\n file:bytes=$guid\n :size?=$object.size\n :name?=$object.name\n :md5?=$object.hashes.\"MD5\"\n :sha1?=$object.hashes.\"SHA-1\"\n :sha256?=$object.hashes.\"SHA-256\"\n :sha512?=$object.hashes.\"SHA-512\"\n ]\n // TODO - Do something with windows-pebinary-ext extension\n [ <(refs)+ { it:sec:stix:bundle:id=$bundle.id } ]\n $node.data.set(stix:object, $object)\n return($node)\n}"
},
"grouping": {
"storm": "// TODO - Figure out what these look like."
},
"identity": {
"storm": "switch $object.identity_class {\n group: {[ ps:contact=(stix, identity, $object.id) :orgname?=$object.name ]}\n organization: {[ ps:contact=(stix, identity, $object.id) :orgname?=$object.name ]}\n individual: {[ ps:contact=(stix, identity, $object.id) :name?=$object.name ]}\n system: {[ it:host=(stix, identity, $object.id) :name?=$object.name ]}\n}\n[ <(refs)+ { it:sec:stix:bundle:id=$bundle.id } ]\n$node.data.set(stix:object, $object)\nreturn($node)"
},
"incident": {
"storm": "// TODO - Figure out what these look like."
},
"indicator": {
"storm": "$guid = $lib.guid(stix, indicator, $object.id)\nswitch $object.pattern_type {\n\n yara: {[ it:app:yara:rule=$guid\n :name?=$object.name\n :text?=$object.pattern\n ]}\n\n snort: {[ it:app:snort:rule=$guid\n :name?=$object.name\n :text?=$object.pattern\n ]}\n\n *: {[ it:sec:stix:indicator=$guid\n :name?=$object.name\n :pattern?=$object.pattern\n :created?=$object.created\n :updated?=$object.modified]\n | scrape --refs :pattern\n }\n}\n[ <(refs)+ { it:sec:stix:bundle:id=$bundle.id } ]\n$node.data.set(stix:object, $object)\nreturn($node)"
},
"infrastructure": {
"storm": "// TODO - Figure out what these look like."
},
"intrusion-set": {
"storm": "($ok, $name) = $lib.trycast(ou:name, $object.name)\nif $ok {\n\n ou:name=$name -> ou:org\n { for $alias in $object.aliases { [ :names?+=$alias ] } }\n [ <(refs)+ { it:sec:stix:bundle:id=$bundle.id } ]\n return($node)\n\n [ ou:org=* :name=$name ]\n { for $alias in $object.aliases { [ :names?+=$alias ] } }\n [ <(refs)+ { it:sec:stix:bundle:id=$bundle.id } ]\n $node.data.set(stix:object, $object)\n return($node)\n}"
},
"ipv4-addr": {
"storm": "[inet:ipv4=$object.value]\n[ <(refs)+ { it:sec:stix:bundle:id=$bundle.id } ]\n$node.data.set(stix:object, $object)\nreturn($node)"
},
"ipv6-addr": {
"storm": "[inet:ipv6=$object.value]\n[ <(refs)+ { it:sec:stix:bundle:id=$bundle.id } ]\n$node.data.set(stix:object, $object)\nreturn($node)"
},
"location": {
"storm": "// TODO - Figure out what these look like."
},
"mac-addr": {
"storm": "// TODO - Figure out what these look like."
},
"malware-analysis": {
"storm": "[\n it:av:scan:result=$lib.guid(stix, \"malware-analysis\", $object.id)\n :verdict=$object.result\n :signame=$object.result_name\n :scanner:name=$object.product\n]\n[ <(refs)+ { it:sec:stix:bundle:id=$bundle.id } ]\n$node.data.set(stix:object, $object)\nreturn($node)"
},
"malware": {
"storm": "($ok, $name) = $lib.trycast(it:prod:softname, $object.name)\nif $ok {\n it:prod:softname=$name -> it:prod:soft\n [ <(refs)+ { it:sec:stix:bundle:id=$bundle.id } ]\n return($node)\n [ it:prod:soft=* :name=$name ]\n [ <(refs)+ { it:sec:stix:bundle:id=$bundle.id } ]\n $node.data.set(stix:object, $object)\n return($node)\n} else { // Sometimes malware objects don't have a name, let's save the description as a meta:note instead.\n [\n meta:note=(stix, note, $object.id)\n :type=stix.malware\n :text=$object.description\n :created=$object.created\n :updated=$object.modified\n ]\n [ <(refs)+ { it:sec:stix:bundle:id=$bundle.id } ]\n $node.data.set(stix:object, $object)\n return($node)\n}"
},
"mutex": {
"storm": "// TODO - Figure out what these look like."
},
"network-traffic": {
"storm": "// TODO - Figure out what these look like."
},
"note": {
"storm": "[\n meta:note=(stix, note, $object.id)\n :type=stix.note\n :text=$object.content\n :created=$object.created\n :updated=$object.modified\n]\n[ <(refs)+ { it:sec:stix:bundle:id=$bundle.id } ]\nreturn($node)"
},
"observed-data": {
"storm": "// TODO - Figure out what these look like."
},
"opinion": {
"storm": "// TODO - Figure out if content is the right key here.\n[\n meta:note=(stix, note, $object.id)\n :type=stix.opinion\n :text=$object.content\n :created=$object.created\n :updated=$object.modified\n]\n[ <(refs)+ { it:sec:stix:bundle:id=$bundle.id } ]\nreturn($node)"
},
"process": {
"storm": "// TODO - Figure out what these look like."
},
"relationship": {
"storm": "// TODO - Figure out what these look like."
},
"report": {
"storm": "[ media:news=(stix, report, $object.id)\n :title?=$object.name\n :summary?=$object.description\n :published?=$object.published\n]\n[ <(refs)+ { it:sec:stix:bundle:id=$bundle.id } ]\n$node.data.set(stix:object, $object)\nreturn($node)"
},
"sighting": {
"storm": "// TODO - Figure out what these look like."
},
"software": {
"storm": "// TODO - Figure out what these look like."
},
"threat-actor": {
"storm": "[ ps:contact=(stix, threat-actor, $object.id)\n :name?=$object.name\n :desc?=$object.description\n :names?=$object.aliases\n]\n[ <(refs)+ { it:sec:stix:bundle:id=$bundle.id } ]\n$node.data.set(stix:object, $object)\nreturn($node)"
},
"tool": {
"storm": "($ok, $name) = $lib.trycast(it:prod:softname, $object.name)\nif $ok {\n it:prod:softname=$name -> it:prod:soft\n [ <(refs)+ { it:sec:stix:bundle:id=$bundle.id } ]\n return($node)\n [ it:prod:soft=* :name=$name ]\n [ <(refs)+ { it:sec:stix:bundle:id=$bundle.id } ]\n return($node)\n}"
},
"url": {
"storm": "// TODO - Make sure value is the right key for this\n[inet:url=`{$object.value}`]\n[ <(refs)+ { it:sec:stix:bundle:id=$bundle.id } ]\n$node.data.set(stix:object, $object)\nreturn($node)"
},
"user-account": {
"storm": ""
},
"vulnerability": {
"storm": "// TODO - Figure out what these look like."
},
"windows-registry-key": {
"storm": "// TODO - Figure out what these look like."
},
"x509-certificate": {
"storm": "// TODO - Figure out what these look like."
}
},
"relationships": [
{
"type": [
"campaign",
"attributed-to",
"intrusion-set"
],
"storm": "$n1node.props.org = $n2node"
},
{
"type": [
"intrusion-set",
"attributed-to",
"threat-actor"
],
"storm": "$n2node.props.org = $n1node"
},
{
"type": [
null,
"uses",
null
],
"storm": "yield $n1node [ +(uses)> { yield $n2node } ]"
},
{
"type": [
null,
"indicates",
null
],
"storm": "yield $n1node [ +(indicates)> { yield $n2node } ]"
},
{
"type": [
"threat-actor",
"attributed-to",
"identity"
],
"storm": ""
}
]
}
8 changes: 8 additions & 0 deletions scripts/build_stix_conf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
"""Build a default stix bundle Storm import config and save it to ``data/stix_import_conf.json``."""

import stormlibpp.stix


bc = stormlibpp.stix.BundleConf()
bc.load()
bc.save(out="data/stix_import_conf.json")
11 changes: 11 additions & 0 deletions src/stormlibpp/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,14 @@ class HttpCortexNotImplementedError(s_exc.SynErr, NotImplementedError):

class HttpCortexJsonError(HttpCortexError):
"""Unable to parse JSON response from Synapse with HttpCortex."""

class BundleConfError(RuntimeError):
"""An error raised by BundleConf."""


class BundleConfSaveError(BundleConfError):
"""Raise when a BundleConf can't be saved."""


class BundleConfLoadStormError(BundleConfError):
"""Raise when a BundleConf can't load a Storm file."""
107 changes: 107 additions & 0 deletions src/stormlibpp/stix/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
"""Code to aid in building objects for the ``$lib.stix`` Storm library."""

import json
import pathlib

from . import storm_mapping
from .. import errors
from .. import utils


BLANK_CONF = {
"addbundle": True,
"bundle": {},
"objects": {},
"relationships": [],
}
"""A blank ``$lib.stix.import.config()``."""


class BundleConf:
"""Create a ``$lib.stix.import.config()`` compatible config from storm files.

Parameters
----------
storm_path : str, optional
The path the to folder containing Storm code, by default "storm".
default : bool, optional
Whether to build the default import config dict packaged with stormlibpp.
Ignores the ``storm_path`` object if True. By default True.
out : str, optional
The path to save output in, by default "bundle_conf.json".
map : dict, optional
A custom mapping of ``$lib.stix.import.config()`` to Storm files. By
default ``storm_mapping.MAP``. See ``storm_mapping.MAP`` docstrings for details.
"""

def __init__(
self,
storm_path: str = "storm",
default: bool = True,
out: str = "bundle_conf.json",
map: dict = storm_mapping.MAP,
) -> None:
self.default = default

if self.default:
self.path = (pathlib.Path(__file__).parent / "storm").expanduser().resolve()
else:
self.path = pathlib.Path(storm_path).expanduser().resolve()

self.out = pathlib.Path(out).expanduser().resolve()

self.map = map

self.bundle_conf = BLANK_CONF

def __repr__(self):
return (
"BundleConf(storm_path='{p}', out='{o}', default={d}, map={m})"
).format(
p=self.path,
o=self.out,
d=self.default,
m="{Default Map}" if self.map == storm_mapping.MAP else "{Custom Map}"
)

def __str__(self) -> str:
return json.dumps(self.bundle_conf)

def _load_storm(self, key: str, rel_path: str):
path = str(self.path / key / rel_path)

try:
with open(path, "r") as fd:
storm = fd.read()
utils.chk_storm_syntax(storm)
except OSError as err:
raise errors.BundleConfLoadStormError(
f"Unable to load the Storm code from {path}: {err}"
) from err
except errors.StormSyntaxError as err:
raise errors.BundleConfLoadStormError(
f"Storm syntax error in {path}: {err}"
) from err

return storm

def load(self):
for key, val in self.map.items():
if key == "bundle":
self.bundle_conf[key].update({"storm": self._load_storm(key, val)})
elif key == "relationships":
for item in val:
self.bundle_conf[key].append({"type": item["type"], "storm": self._load_storm(key, item["storm"])})
elif key == "objects":
for item in val:
self.bundle_conf[key].update({item: {"storm": self._load_storm(key, val[item])}})

def save(self, out: str | pathlib.Path = ""):
path = out if out else self.out
try:
with open(path, "w") as fd:
json.dump(self.bundle_conf, fd, indent=2)
except (TypeError, OSError) as err:
raise errors.BundleConfSaveError(
f"Unable to save the Storm Stix bundle config to {path}: {err}"
) from err
62 changes: 62 additions & 0 deletions src/stormlibpp/stix/storm/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Storm Stix Conversion Code

Storm code included in a custom `$lib.stix.import.config` dict that is generated by `stormlibpp.stix`. The intent is to create an object for as many objects that Stix supports as possible.

## License Notice

Currently, some files in this folder (listed below) are derivatives of the `synapse.lib.stormlib.stix` module - specifically, [this code](https://github.com/vertexproject/synapse/blob/2a2fd0549956c3754e88128a215e3910eb68176e/synapse/lib/stormlib/stix.py#L679). These files are covered by this [license](https://github.com/vertexproject/synapse/blob/2a2fd0549956c3754e88128a215e3910eb68176e/LICENSE).

- `bundle/bundle.storm`
- `objects/campaign.storm`
- `objects/course-of-action.storm`
- `objects/identity.storm`
- `objects/indicator.storm`
- `objects/intrusion-set.storm`
- `objects/malware.storm`
- `objects/report.storm`
- `objects/threat-actor.storm`
- `objects/tool.storm`
- `relationships/camp_to_intset.storm`
- `relationships/indicates.storm`
- `relationships/intset_to_ta.storm`
- `relationships/ta_to_id.storm`
- `relationships/uses.storm`

CHANGES:
- `bundle/bundle.storm`
- None
- `objects/campaign.storm`
- Refs edge to the stix bundle
- `objects/course-of-action.storm`
- Refs edge to the stix bundle
- `objects/identity.storm`
- Refs edge to the stix bundle
- Include the stix object in nodedata - with the bundle ID
- `objects/indicator.storm`
- Refs edge to the stix bundle
- `objects/intrusion-set.storm`
- Refs edge to the stix bundle
- Include the stix object in nodedata - with the bundle ID
- `objects/malware.storm`
- Refs edge to the stix bundle
- Include the stix object in nodedata for new nodes only - with the bundle ID
- Save malware objects without a name as meta:note nodes
- `objects/report.storm`
- Refs edge to the stix bundle
- Include the bundle ID in stix object nodedata
- `objects/threat-actor.storm`
- Refs edge to the stix bundle
- Include the bundle ID in stix object nodedata
- `objects/tool.storm`
- Refs edge to the stix bundle
- Include the stix object in nodedata for new nodes only - with the bundle ID
- `relationships/camp_to_intset.storm`
- None
- `relationships/indicates.storm`
- None
- `relationships/intset_to_ta.storm`
- None
- `relationships/ta_to_id.storm`
- None
- `relationships/uses.storm`
- None
5 changes: 5 additions & 0 deletions src/stormlibpp/stix/storm/bundle/bundle.storm
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
it:sec:stix:bundle:id=$bundle.id
return($node)
[ it:sec:stix:bundle=* :id=$bundle.id ]
$node.data.set(stix:bundle, $object)
return($node)
Loading