txt2stix 0.0.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- txt2stix/__init__.py +33 -0
- txt2stix/ai_extractor/__init__.py +15 -0
- txt2stix/ai_extractor/anthropic.py +12 -0
- txt2stix/ai_extractor/base.py +87 -0
- txt2stix/ai_extractor/deepseek.py +19 -0
- txt2stix/ai_extractor/gemini.py +18 -0
- txt2stix/ai_extractor/openai.py +15 -0
- txt2stix/ai_extractor/openrouter.py +20 -0
- txt2stix/ai_extractor/prompts.py +164 -0
- txt2stix/ai_extractor/utils.py +85 -0
- txt2stix/attack_flow.py +101 -0
- txt2stix/bundler.py +428 -0
- txt2stix/common.py +23 -0
- txt2stix/extractions.py +59 -0
- txt2stix/includes/__init__.py +0 -0
- txt2stix/includes/extractions/ai/config.yaml +1023 -0
- txt2stix/includes/extractions/lookup/config.yaml +393 -0
- txt2stix/includes/extractions/pattern/config.yaml +609 -0
- txt2stix/includes/helpers/mimetype_filename_extension_list.csv +936 -0
- txt2stix/includes/helpers/stix_relationship_types.txt +41 -0
- txt2stix/includes/helpers/tlds.txt +1446 -0
- txt2stix/includes/helpers/windows_registry_key_prefix.txt +12 -0
- txt2stix/includes/lookups/_README.md +11 -0
- txt2stix/includes/lookups/_generate_lookups.py +247 -0
- txt2stix/includes/lookups/attack_pattern.txt +1 -0
- txt2stix/includes/lookups/campaign.txt +1 -0
- txt2stix/includes/lookups/country_iso3166_alpha2.txt +249 -0
- txt2stix/includes/lookups/course_of_action.txt +1 -0
- txt2stix/includes/lookups/disarm_id_v1_5.txt +345 -0
- txt2stix/includes/lookups/disarm_name_v1_5.txt +347 -0
- txt2stix/includes/lookups/extensions.txt +78 -0
- txt2stix/includes/lookups/identity.txt +1 -0
- txt2stix/includes/lookups/infrastructure.txt +1 -0
- txt2stix/includes/lookups/intrusion_set.txt +1 -0
- txt2stix/includes/lookups/malware.txt +2 -0
- txt2stix/includes/lookups/mitre_atlas_id_v4_5_2.txt +116 -0
- txt2stix/includes/lookups/mitre_atlas_name_v4_5_2.txt +117 -0
- txt2stix/includes/lookups/mitre_attack_enterprise_aliases_v16_0.txt +1502 -0
- txt2stix/includes/lookups/mitre_attack_enterprise_id_v16_0.txt +1656 -0
- txt2stix/includes/lookups/mitre_attack_enterprise_name_v16_0.txt +1765 -0
- txt2stix/includes/lookups/mitre_attack_ics_aliases_v16_0.txt +141 -0
- txt2stix/includes/lookups/mitre_attack_ics_id_v16_0.txt +254 -0
- txt2stix/includes/lookups/mitre_attack_ics_name_v16_0.txt +293 -0
- txt2stix/includes/lookups/mitre_attack_mobile_aliases_v16_0.txt +159 -0
- txt2stix/includes/lookups/mitre_attack_mobile_id_v16_0.txt +277 -0
- txt2stix/includes/lookups/mitre_attack_mobile_name_v16_0.txt +296 -0
- txt2stix/includes/lookups/mitre_capec_id_v3_9.txt +559 -0
- txt2stix/includes/lookups/mitre_capec_name_v3_9.txt +560 -0
- txt2stix/includes/lookups/mitre_cwe_id_v4_15.txt +939 -0
- txt2stix/includes/lookups/mitre_cwe_name_v4_15.txt +939 -0
- txt2stix/includes/lookups/threat_actor.txt +1 -0
- txt2stix/includes/lookups/tld.txt +1422 -0
- txt2stix/includes/lookups/tool.txt +1 -0
- txt2stix/includes/tests/test_cases.yaml +695 -0
- txt2stix/indicator.py +860 -0
- txt2stix/lookups.py +68 -0
- txt2stix/pattern/__init__.py +13 -0
- txt2stix/pattern/extractors/__init__.py +0 -0
- txt2stix/pattern/extractors/base_extractor.py +167 -0
- txt2stix/pattern/extractors/card/README.md +34 -0
- txt2stix/pattern/extractors/card/__init__.py +15 -0
- txt2stix/pattern/extractors/card/amex_card_extractor.py +52 -0
- txt2stix/pattern/extractors/card/diners_card_extractor.py +47 -0
- txt2stix/pattern/extractors/card/discover_card_extractor.py +48 -0
- txt2stix/pattern/extractors/card/jcb_card_extractor.py +43 -0
- txt2stix/pattern/extractors/card/master_card_extractor.py +63 -0
- txt2stix/pattern/extractors/card/union_card_extractor.py +38 -0
- txt2stix/pattern/extractors/card/visa_card_extractor.py +46 -0
- txt2stix/pattern/extractors/crypto/__init__.py +3 -0
- txt2stix/pattern/extractors/crypto/btc_extractor.py +38 -0
- txt2stix/pattern/extractors/directory/__init__.py +10 -0
- txt2stix/pattern/extractors/directory/unix_directory_extractor.py +40 -0
- txt2stix/pattern/extractors/directory/unix_file_path_extractor.py +42 -0
- txt2stix/pattern/extractors/directory/windows_directory_path_extractor.py +47 -0
- txt2stix/pattern/extractors/directory/windows_file_path_extractor.py +42 -0
- txt2stix/pattern/extractors/domain/__init__.py +8 -0
- txt2stix/pattern/extractors/domain/domain_extractor.py +39 -0
- txt2stix/pattern/extractors/domain/hostname_extractor.py +36 -0
- txt2stix/pattern/extractors/domain/sub_domain_extractor.py +49 -0
- txt2stix/pattern/extractors/hashes/__init__.py +16 -0
- txt2stix/pattern/extractors/hashes/md5_extractor.py +16 -0
- txt2stix/pattern/extractors/hashes/sha1_extractor.py +14 -0
- txt2stix/pattern/extractors/hashes/sha224_extractor.py +18 -0
- txt2stix/pattern/extractors/hashes/sha2_256_exactor.py +14 -0
- txt2stix/pattern/extractors/hashes/sha2_512_exactor.py +13 -0
- txt2stix/pattern/extractors/hashes/sha3_256_exactor.py +15 -0
- txt2stix/pattern/extractors/hashes/sha3_512_exactor.py +16 -0
- txt2stix/pattern/extractors/helper.py +64 -0
- txt2stix/pattern/extractors/ip/__init__.py +14 -0
- txt2stix/pattern/extractors/ip/ipv4_cidr_extractor.py +49 -0
- txt2stix/pattern/extractors/ip/ipv4_extractor.py +18 -0
- txt2stix/pattern/extractors/ip/ipv4_port_extractor.py +42 -0
- txt2stix/pattern/extractors/ip/ipv6_cidr_extractor.py +18 -0
- txt2stix/pattern/extractors/ip/ipv6_extractor.py +16 -0
- txt2stix/pattern/extractors/ip/ipv6_port_extractor.py +46 -0
- txt2stix/pattern/extractors/others/__init__.py +22 -0
- txt2stix/pattern/extractors/others/asn_extractor.py +14 -0
- txt2stix/pattern/extractors/others/cpe_extractor.py +29 -0
- txt2stix/pattern/extractors/others/cve_extractor.py +14 -0
- txt2stix/pattern/extractors/others/email_extractor.py +21 -0
- txt2stix/pattern/extractors/others/filename_extractor.py +17 -0
- txt2stix/pattern/extractors/others/iban_extractor.py +15 -0
- txt2stix/pattern/extractors/others/mac_address_extractor.py +13 -0
- txt2stix/pattern/extractors/others/phonenumber_extractor.py +41 -0
- txt2stix/pattern/extractors/others/user_agent_extractor.py +20 -0
- txt2stix/pattern/extractors/others/windows_registry_key_extractor.py +18 -0
- txt2stix/pattern/extractors/url/__init__.py +7 -0
- txt2stix/pattern/extractors/url/url_extractor.py +22 -0
- txt2stix/pattern/extractors/url/url_file_extractor.py +21 -0
- txt2stix/pattern/extractors/url/url_path_extractor.py +74 -0
- txt2stix/retriever.py +126 -0
- txt2stix/stix.py +1 -0
- txt2stix/txt2stix.py +336 -0
- txt2stix/utils.py +86 -0
- txt2stix-0.0.4.dist-info/METADATA +190 -0
- txt2stix-0.0.4.dist-info/RECORD +119 -0
- txt2stix-0.0.4.dist-info/WHEEL +4 -0
- txt2stix-0.0.4.dist-info/entry_points.txt +2 -0
- txt2stix-0.0.4.dist-info/licenses/LICENSE +202 -0
txt2stix/__init__.py
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
from txt2stix import extractions
|
2
|
+
from .bundler import txt2stixBundler
|
3
|
+
from .txt2stix import extract_all
|
4
|
+
from pathlib import Path
|
5
|
+
|
6
|
+
INCLUDES_PATH = None
|
7
|
+
def get_include_path():
|
8
|
+
global INCLUDES_PATH
|
9
|
+
|
10
|
+
if INCLUDES_PATH:
|
11
|
+
return INCLUDES_PATH
|
12
|
+
|
13
|
+
from pathlib import Path
|
14
|
+
MODULE_PATH = Path(__file__).parent.parent
|
15
|
+
INCLUDES_PATH = MODULE_PATH/"includes"
|
16
|
+
try:
|
17
|
+
from . import includes
|
18
|
+
INCLUDES_PATH = Path(includes.__file__).parent
|
19
|
+
except:
|
20
|
+
pass
|
21
|
+
return INCLUDES_PATH
|
22
|
+
|
23
|
+
def set_include_path(path):
|
24
|
+
global INCLUDES_PATH
|
25
|
+
INCLUDES_PATH = path
|
26
|
+
|
27
|
+
def get_all_extractors(include_path=None):
|
28
|
+
return extractions.parse_extraction_config(include_path or get_include_path())
|
29
|
+
|
30
|
+
|
31
|
+
__all__ = [
|
32
|
+
'txt2stixBundler', 'extract_all', 'get_include_path'
|
33
|
+
]
|
@@ -0,0 +1,15 @@
|
|
1
|
+
import logging
|
2
|
+
|
3
|
+
import dotenv
|
4
|
+
|
5
|
+
from .base import _ai_extractor_registry as ALL_AI_EXTRACTORS
|
6
|
+
|
7
|
+
from .base import BaseAIExtractor
|
8
|
+
class ModelError(Exception):
|
9
|
+
pass
|
10
|
+
|
11
|
+
for path in ["openai", "anthropic", "gemini", "deepseek", "openrouter"]:
|
12
|
+
try:
|
13
|
+
__import__(__package__ + "." + path)
|
14
|
+
except Exception as e:
|
15
|
+
logging.warning("%s not supported, please install missing modules", path, exc_info=True)
|
@@ -0,0 +1,12 @@
|
|
1
|
+
|
2
|
+
import logging
|
3
|
+
import os
|
4
|
+
from txt2stix.ai_extractor.base import BaseAIExtractor
|
5
|
+
from llama_index.llms.anthropic import Anthropic
|
6
|
+
|
7
|
+
|
8
|
+
class AnthropicAIExtractor(BaseAIExtractor, provider="anthropic"):
|
9
|
+
def __init__(self, **kwargs) -> None:
|
10
|
+
kwargs.setdefault('temperature', float(os.environ.get('TEMPERATURE', 0.0)))
|
11
|
+
self.llm = Anthropic(max_tokens=4096, system_prompt=self.system_prompt, **kwargs)
|
12
|
+
super().__init__()
|
@@ -0,0 +1,87 @@
|
|
1
|
+
import logging
|
2
|
+
from typing import Type
|
3
|
+
from llama_index.core.program import LLMTextCompletionProgram
|
4
|
+
|
5
|
+
import textwrap
|
6
|
+
from llama_index.core import PromptTemplate
|
7
|
+
from llama_index.core.llms.llm import LLM
|
8
|
+
|
9
|
+
from txt2stix.ai_extractor.prompts import DEFAULT_CONTENT_CHECKER_TEMPL, DEFAULT_EXTRACTION_TEMPL, DEFAULT_RELATIONSHIP_TEMPL, DEFAULT_SYSTEM_PROMPT, ATTACK_FLOW_PROMPT_TEMPL
|
10
|
+
from txt2stix.ai_extractor.utils import AttackFlowList, DescribesIncident, ExtractionList, ParserWithLogging, RelationshipList, get_extractors_str
|
11
|
+
from llama_index.core.utils import get_tokenizer
|
12
|
+
|
13
|
+
|
14
|
+
_ai_extractor_registry: dict[str, 'Type[BaseAIExtractor]'] = {}
|
15
|
+
class BaseAIExtractor():
|
16
|
+
system_prompt = DEFAULT_SYSTEM_PROMPT
|
17
|
+
|
18
|
+
extraction_template = DEFAULT_EXTRACTION_TEMPL
|
19
|
+
|
20
|
+
relationship_template = DEFAULT_RELATIONSHIP_TEMPL
|
21
|
+
|
22
|
+
content_check_template = DEFAULT_CONTENT_CHECKER_TEMPL
|
23
|
+
|
24
|
+
def _get_extraction_program(self):
|
25
|
+
return LLMTextCompletionProgram.from_defaults(
|
26
|
+
output_parser=ParserWithLogging(ExtractionList),
|
27
|
+
prompt=self.extraction_template,
|
28
|
+
verbose=True,
|
29
|
+
llm=self.llm,
|
30
|
+
)
|
31
|
+
|
32
|
+
def _get_relationship_program(self):
|
33
|
+
return LLMTextCompletionProgram.from_defaults(
|
34
|
+
output_parser=ParserWithLogging(RelationshipList),
|
35
|
+
prompt=self.relationship_template,
|
36
|
+
verbose=True,
|
37
|
+
llm=self.llm,
|
38
|
+
)
|
39
|
+
|
40
|
+
def _get_content_checker_program(self):
|
41
|
+
return LLMTextCompletionProgram.from_defaults(
|
42
|
+
output_parser=ParserWithLogging(DescribesIncident),
|
43
|
+
prompt=self.content_check_template,
|
44
|
+
verbose=True,
|
45
|
+
llm=self.llm,
|
46
|
+
)
|
47
|
+
|
48
|
+
def check_content(self, text) -> DescribesIncident:
|
49
|
+
return self._get_content_checker_program()(context_str=text)
|
50
|
+
|
51
|
+
def _get_attack_flow_program(self):
|
52
|
+
return LLMTextCompletionProgram.from_defaults(
|
53
|
+
output_parser=ParserWithLogging(AttackFlowList),
|
54
|
+
prompt=ATTACK_FLOW_PROMPT_TEMPL,
|
55
|
+
verbose=True,
|
56
|
+
llm=self.llm,
|
57
|
+
)
|
58
|
+
|
59
|
+
def extract_attack_flow(self, input_text, extractions, relationships) -> AttackFlowList:
|
60
|
+
return self._get_attack_flow_program()(document=input_text, extractions=extractions, relationships=relationships)
|
61
|
+
|
62
|
+
def extract_relationships(self, input_text, extractions, relationship_types: list[str]) -> RelationshipList:
|
63
|
+
return self._get_relationship_program()(relationship_types=relationship_types, input_file=input_text, extractions=extractions)
|
64
|
+
|
65
|
+
def extract_objects(self, input_text, extractors) -> ExtractionList:
|
66
|
+
return self._get_extraction_program()(extractors=get_extractors_str(extractors), input_file=input_text)
|
67
|
+
|
68
|
+
def __init__(self, *args, **kwargs) -> None:
|
69
|
+
pass
|
70
|
+
|
71
|
+
def count_tokens(self, input_text):
|
72
|
+
logging.info("unsupported model `%s`, estimating using llama-index's default tokenizer", self.extractor_name)
|
73
|
+
return len(get_tokenizer()(input_text))
|
74
|
+
|
75
|
+
def __init_subclass__(cls, /, provider, register=True, **kwargs):
|
76
|
+
super().__init_subclass__(**kwargs)
|
77
|
+
if register:
|
78
|
+
cls.provider = provider
|
79
|
+
_ai_extractor_registry[provider] = cls
|
80
|
+
|
81
|
+
@property
|
82
|
+
def extractor_name(self):
|
83
|
+
return f"{self.provider}:{self.llm.model}"
|
84
|
+
|
85
|
+
|
86
|
+
def __hash__(self):
|
87
|
+
return hash(self.extractor_name)
|
@@ -0,0 +1,19 @@
|
|
1
|
+
import logging
|
2
|
+
import os
|
3
|
+
|
4
|
+
from .base import BaseAIExtractor
|
5
|
+
from llama_index.llms.deepseek import DeepSeek
|
6
|
+
|
7
|
+
class DeepseekExtractor(BaseAIExtractor, provider='deepseek'):
|
8
|
+
def __init__(self, **kwargs) -> None:
|
9
|
+
kwargs.setdefault('temperature', float(os.environ.get('TEMPERATURE', 0.0)))
|
10
|
+
kwargs.setdefault('model', 'deepseek-chat')
|
11
|
+
self.llm = DeepSeek(system_prompt=self.system_prompt, **kwargs)
|
12
|
+
super().__init__()
|
13
|
+
|
14
|
+
def count_tokens(self, text):
|
15
|
+
try:
|
16
|
+
return len(self.llm._tokenizer.encode(text))
|
17
|
+
except Exception as e:
|
18
|
+
logging.warning(e)
|
19
|
+
return super().count_tokens(text)
|
@@ -0,0 +1,18 @@
|
|
1
|
+
|
2
|
+
import os
|
3
|
+
from txt2stix.ai_extractor.base import BaseAIExtractor
|
4
|
+
from llama_index.llms.gemini import Gemini
|
5
|
+
|
6
|
+
|
7
|
+
class GeminiAIExtractor(BaseAIExtractor, provider="gemini"):
|
8
|
+
def __init__(self, **kwargs) -> None:
|
9
|
+
kwargs.setdefault('temperature', float(os.environ.get('TEMPERATURE', 0.0)))
|
10
|
+
self.llm = Gemini(max_tokens=4096, **kwargs)
|
11
|
+
super().__init__()
|
12
|
+
|
13
|
+
def count_tokens(self, text):
|
14
|
+
return self.llm._model.count_tokens(text).total_tokens
|
15
|
+
|
16
|
+
@property
|
17
|
+
def extractor_name(self):
|
18
|
+
return f"{self.provider}:{self.llm.model}"
|
@@ -0,0 +1,15 @@
|
|
1
|
+
|
2
|
+
import os
|
3
|
+
from txt2stix.ai_extractor.base import BaseAIExtractor
|
4
|
+
from llama_index.llms.openai import OpenAI
|
5
|
+
|
6
|
+
|
7
|
+
class OpenAIExtractor(BaseAIExtractor, provider="openai"):
|
8
|
+
def __init__(self, **kwargs) -> None:
|
9
|
+
kwargs.setdefault('temperature', float(os.environ.get('TEMPERATURE', 0.0)))
|
10
|
+
self.llm = OpenAI(system_prompt=self.system_prompt, **kwargs)
|
11
|
+
super().__init__()
|
12
|
+
|
13
|
+
def count_tokens(self, text):
|
14
|
+
return len(self.llm._tokenizer.encode(text))
|
15
|
+
|
@@ -0,0 +1,20 @@
|
|
1
|
+
|
2
|
+
import logging
|
3
|
+
import os
|
4
|
+
from .base import BaseAIExtractor
|
5
|
+
from llama_index.llms.openrouter import OpenRouter
|
6
|
+
|
7
|
+
|
8
|
+
class OpenRouterExtractor(BaseAIExtractor, provider="openrouter"):
|
9
|
+
def __init__(self, **kwargs) -> None:
|
10
|
+
kwargs.setdefault('temperature', float(os.environ.get('TEMPERATURE', 0.0)))
|
11
|
+
self.llm = OpenRouter(system_prompt=self.system_prompt, **kwargs)
|
12
|
+
super().__init__()
|
13
|
+
|
14
|
+
def count_tokens(self, text):
|
15
|
+
try:
|
16
|
+
return len(self.llm._tokenizer.encode(text))
|
17
|
+
except Exception as e:
|
18
|
+
logging.warning(e)
|
19
|
+
return super().count_tokens(text)
|
20
|
+
|
@@ -0,0 +1,164 @@
|
|
1
|
+
|
2
|
+
from llama_index.core import PromptTemplate, ChatPromptTemplate
|
3
|
+
import textwrap
|
4
|
+
from llama_index.core.base.llms.types import ChatMessage, MessageRole
|
5
|
+
|
6
|
+
|
7
|
+
DEFAULT_SYSTEM_PROMPT = textwrap.dedent(
|
8
|
+
"""
|
9
|
+
<persona>
|
10
|
+
|
11
|
+
You are a cyber-security threat intelligence analysis tool responsible for analysing intelligence provided in text files.
|
12
|
+
|
13
|
+
You have a deep understanding of cybersecurity and threat intelligence concepts.
|
14
|
+
|
15
|
+
IMPORTANT: You must always deliver your work as a computer-parsable output in JSON format. All output from you will be parsed with pydantic for further processing.
|
16
|
+
|
17
|
+
</persona>
|
18
|
+
"""
|
19
|
+
)
|
20
|
+
|
21
|
+
DEFAULT_EXTRACTION_TEMPL = PromptTemplate(textwrap.dedent(
|
22
|
+
"""
|
23
|
+
<persona>
|
24
|
+
You are a cyber-security threat intelligence analysis tool responsible for analysing intelligence provided in text files.
|
25
|
+
You have a deep understanding of cybersecurity and threat intelligence concepts.
|
26
|
+
IMPORTANT: You must always deliver your work as a computer-parsable output in JSON format. All output from you will be parsed with pydantic for further processing.
|
27
|
+
</persona>
|
28
|
+
<requirements>
|
29
|
+
Using the report text printed between the `<document>` tags, you should extract the Indicators of Compromise (IoCs) and Tactics, Techniques, and Procedures (TTPs) being described in it.
|
30
|
+
The document can contain the same IOC or TTP one or more times. Only create one record for each extraction -- the extractions must be unique!
|
31
|
+
Only one JSON object should exist for each unique value.
|
32
|
+
</requirements>
|
33
|
+
<accuracy>
|
34
|
+
Think about your answer first before you respond. The accuracy of your response is very important as this data will be used for operational purposes.
|
35
|
+
If you don't know the answer, reply with success: false, do not ever try to make up an answer.
|
36
|
+
</accuracy>
|
37
|
+
<document>
|
38
|
+
{input_file}
|
39
|
+
</document>
|
40
|
+
<extractors>
|
41
|
+
{extractors}
|
42
|
+
</extractors>
|
43
|
+
<response>
|
44
|
+
IMPORTANT: Only include a valid JSON document in your response and no other text. The JSON document should be minified!.
|
45
|
+
Response MUST be in JSON format.
|
46
|
+
Response MUST start with: {"success":
|
47
|
+
</response>
|
48
|
+
"""
|
49
|
+
))
|
50
|
+
|
51
|
+
|
52
|
+
DEFAULT_RELATIONSHIP_TEMPL = PromptTemplate(textwrap.dedent(
|
53
|
+
"""
|
54
|
+
<persona>
|
55
|
+
You are a cyber-security threat intelligence analysis tool responsible for analysing intelligence provided in text files.
|
56
|
+
You have a deep understanding of cybersecurity and threat intelligence concepts.
|
57
|
+
IMPORTANT: You must always deliver your work as a computer-parsable output in JSON format. All output from you will be parsed with pydantic for further processing.
|
58
|
+
</persona>
|
59
|
+
<requirements>
|
60
|
+
The tag `<extractions>` contains all the observables and TTPs that were extracted from the document provided in `<document>`
|
61
|
+
Please capture the relationships between the extractions and describe them using NLP techniques.
|
62
|
+
A relationship MUST have different source_ref and target_ref
|
63
|
+
Select an appropriate relationship_type from `<relationship_types>`.
|
64
|
+
Only use `related-to` or any other vague `relationship_type` as a last resort.
|
65
|
+
The value of relationship_type MUST be clear, and it SHOULD NOT describe everything as related-to each other unless they are related in context of the `<document>
|
66
|
+
IMPORTANT: Only include a valid JSON document in your response and no other text. The JSON document should be minified!.
|
67
|
+
</requirements>
|
68
|
+
<accuracy>
|
69
|
+
Think about your answer first before you respond. The accuracy of your response is very important as this data will be used for operational purposes.
|
70
|
+
If you don't know the answer, reply with success: false, do not ever try to make up an answer.
|
71
|
+
</accuracy>
|
72
|
+
<document>
|
73
|
+
{input_file}
|
74
|
+
</document>
|
75
|
+
<extractions>
|
76
|
+
{extractions}
|
77
|
+
</extractions>
|
78
|
+
<relationship_types>
|
79
|
+
{relationship_types}
|
80
|
+
</relationship_types>
|
81
|
+
<response>
|
82
|
+
IMPORTANT: Only include a valid JSON document in your response and no other text. The JSON document should be minified!.
|
83
|
+
Response MUST be in JSON format.
|
84
|
+
Response MUST start with: {"success":
|
85
|
+
</response>
|
86
|
+
"""
|
87
|
+
))
|
88
|
+
|
89
|
+
DEFAULT_CONTENT_CHECKER_TEMPL = PromptTemplate("""
|
90
|
+
<persona>
|
91
|
+
You are a cyber security threat intelligence analyst.
|
92
|
+
Your job is to review reports that describe a cyber security incidents and/or threat intelligence.
|
93
|
+
Examples include malware analysis, APT group reports, data breaches, vulnerabilities, or Indicators of Compromise.
|
94
|
+
Some of the documents you are given will not be this type of report.
|
95
|
+
I need you to tell me if the text provided does match the type of report you are expecting.
|
96
|
+
</persona>
|
97
|
+
<requirement>
|
98
|
+
Using the MARKDOWN of the report provided in <document>
|
99
|
+
IMPORTANT: the output should be structured as valid JSON.
|
100
|
+
IMPORTANT: output should not be in markdown, it must be a plain JSON text without any code block
|
101
|
+
IMPORTANT: do not include any comment in the output
|
102
|
+
IMPORTANT: output must start with a `{` and end with a `}` and must not contain "```"
|
103
|
+
</requirement>
|
104
|
+
<document>
|
105
|
+
{context_str}
|
106
|
+
</document>
|
107
|
+
<incident_classification>
|
108
|
+
Possible Incident Classifications are
|
109
|
+
* `other` (the report does not fit into any of the following categories)
|
110
|
+
* `apt_group`
|
111
|
+
* `vulnerability`
|
112
|
+
* `data_leak`
|
113
|
+
* `malware`
|
114
|
+
* `ransomware`
|
115
|
+
* `infostealer`
|
116
|
+
* `threat_actor`
|
117
|
+
* `campaign`
|
118
|
+
* `exploit`
|
119
|
+
* `cyber_crime`
|
120
|
+
* `indicator_of_compromise`
|
121
|
+
* `ttp`
|
122
|
+
</incident_classification>
|
123
|
+
""")
|
124
|
+
|
125
|
+
ATTACK_FLOW_PROMPT_TEMPL = ChatPromptTemplate([
|
126
|
+
ChatMessage.from_str("""You are a cyber security threat intelligence analyst.
|
127
|
+
Your job is to review report that describe a cyber security incidents.
|
128
|
+
Examples include malware analysis, APT group reports, data breaches and vulnerabilities.""", MessageRole.SYSTEM),
|
129
|
+
ChatMessage.from_str("Hi, What <document> would you like me to process for you? the message below must contain the document and the document only", MessageRole.ASSISTANT),
|
130
|
+
ChatMessage.from_str("{document}", MessageRole.USER),
|
131
|
+
ChatMessage.from_str("What are the objects that have been extracted (<extractions>) from the document above?", MessageRole.ASSISTANT),
|
132
|
+
ChatMessage.from_str("{extractions}", MessageRole.USER),
|
133
|
+
ChatMessage.from_str("What are the relationships that have been extracted (<relationships>) between the documents?", MessageRole.USER),
|
134
|
+
ChatMessage.from_str("{relationships}", MessageRole.USER),
|
135
|
+
ChatMessage.from_str("What should I do with all the data that have been provided?", MessageRole.ASSISTANT),
|
136
|
+
ChatMessage.from_str("""Consider all the MITRE ATT&CK Objects extracted from the report and the relationships they have to other objects.
|
137
|
+
|
138
|
+
Now I need you to logically define the order of ATT&CK Tactics/Techniques as they are executed in the incident described in the report.
|
139
|
+
|
140
|
+
It is possible that the Techniques extracted are not linked to the relevant MITRE ATT&CK Tactic. You should also assign the correct Tactic to a Technique where a Technique belongs to many ATT&CK Tactics in the ATT&CK Matrix if that can correctly be inferred.
|
141
|
+
|
142
|
+
You should also provide a short overview about how this technique is described in the report as the name, and a longer version in description.
|
143
|
+
|
144
|
+
IMPORTANT: only include the ATT&CK IDs extracted already, do not add any new extractions.
|
145
|
+
|
146
|
+
You should deliver a response in JSON as follows
|
147
|
+
|
148
|
+
[
|
149
|
+
{
|
150
|
+
"position": "<ORDER OF OBJECTS STARTING AT 0",
|
151
|
+
"attack_tactic_id": "<ID>",
|
152
|
+
"attack_technique_id": "<ID>",
|
153
|
+
"name": "<NAME>",
|
154
|
+
"description": "<DESC>"
|
155
|
+
},
|
156
|
+
{
|
157
|
+
"position": "<ORDER OF OBJECTS STARTING AT 0",
|
158
|
+
"attack_tactic_id": "<ID>",
|
159
|
+
"attack_technique_id": "<ID>",
|
160
|
+
"name": "<NAME>",
|
161
|
+
"description": "<DESC>"
|
162
|
+
}
|
163
|
+
]""", MessageRole.USER)
|
164
|
+
])
|
@@ -0,0 +1,85 @@
|
|
1
|
+
import io
|
2
|
+
import json
|
3
|
+
import logging
|
4
|
+
|
5
|
+
import dotenv
|
6
|
+
import textwrap
|
7
|
+
|
8
|
+
from ..extractions import Extractor
|
9
|
+
|
10
|
+
from pydantic import BaseModel, Field, RootModel
|
11
|
+
from llama_index.core.output_parsers import PydanticOutputParser
|
12
|
+
|
13
|
+
class Extraction(BaseModel):
|
14
|
+
type : str = Field(description="is the extraction_key value shown in the list printed earlier in this prompt")
|
15
|
+
id: str = Field(description='is the id of the extraction of the format `"ai-%d" %(position in list)`, it should start from 1 (e.g `"ai-1", "ai-2", ..., "ai-n"`)')
|
16
|
+
value: str = Field(description='is the value extracted from the text')
|
17
|
+
original_text: str = Field(description='is the original text the extraction was made from')
|
18
|
+
# start_index: list[str|int] = Field(description='a list of the index positions of the first character for each matching extraction. Some documents might capture many extractions where `key` and `value` are the same for many entries. This property allows the user to identify how many extractions happened, and where they are in the document.')
|
19
|
+
|
20
|
+
class Relationship(BaseModel):
|
21
|
+
source_ref: str = Field(description='is the id for the source extraction for the relationship (e.g. extraction_1).')
|
22
|
+
target_ref: str = Field(description='is the index for the target extraction for the relationship (e.g. extraction_2).')
|
23
|
+
relationship_type: str = Field(description='is a description of the relationship between target and source.')
|
24
|
+
|
25
|
+
class ExtractionList(BaseModel):
|
26
|
+
extractions: list[Extraction] = Field(default_factory=list)
|
27
|
+
success: bool
|
28
|
+
|
29
|
+
class RelationshipList(BaseModel):
|
30
|
+
relationships: list[Relationship] = Field(default_factory=list)
|
31
|
+
success: bool
|
32
|
+
|
33
|
+
class DescribesIncident(BaseModel):
|
34
|
+
describes_incident: bool = Field(description="does the <document> include malware analysis, APT group reports, data breaches and vulnerabilities?")
|
35
|
+
explanation: str = Field(description="Two or three sentence summary of the incidents it describes OR summary of what it describes instead of an incident")
|
36
|
+
incident_classification : list[str] = Field(description="All the valid incident classifications that describe this document/report")
|
37
|
+
|
38
|
+
class AttackFlowItem(BaseModel):
|
39
|
+
position : int = Field(description="order of object starting at 0")
|
40
|
+
attack_tactic_id : str
|
41
|
+
attack_technique_id : str
|
42
|
+
name: str
|
43
|
+
description: str
|
44
|
+
|
45
|
+
class AttackFlowList(BaseModel):
|
46
|
+
matrix : str = Field(description="one of ics, mobile and enterprise")
|
47
|
+
items : list[AttackFlowItem]
|
48
|
+
success: bool = Field(description="determines if there's any valid flow in <extractions>")
|
49
|
+
|
50
|
+
class ParserWithLogging(PydanticOutputParser):
|
51
|
+
def parse(self, text: str):
|
52
|
+
f = io.StringIO()
|
53
|
+
print("\n"*5 + "=================start=================", file=f)
|
54
|
+
print(text, file=f)
|
55
|
+
print("=================close=================" + "\n"*5, file=f)
|
56
|
+
logging.debug(f.getvalue())
|
57
|
+
return super().parse(text)
|
58
|
+
|
59
|
+
def get_extractors_str(extractors):
|
60
|
+
extractor: Extractor = None
|
61
|
+
buffer = io.StringIO()
|
62
|
+
for extractor in extractors:
|
63
|
+
print(f"<extractor name={repr(extractor.name)} extraction_key={repr(extractor.extraction_key)}>", file=buffer)
|
64
|
+
print(f"- {extractor.prompt_base}", file=buffer)
|
65
|
+
if extractor.prompt_helper:
|
66
|
+
print(f"- {extractor.prompt_helper}", file=buffer)
|
67
|
+
if extractor.prompt_conversion:
|
68
|
+
print(f"- {extractor.prompt_conversion}", file=buffer)
|
69
|
+
if extractor.prompt_positive_examples:
|
70
|
+
print(f"- Here are some examples of what SHOULD be extracted for {extractor.name} extractions: {json.dumps(extractor.prompt_positive_examples)}", file=buffer)
|
71
|
+
if extractor.prompt_negative_examples:
|
72
|
+
print(f"- Here are some examples of what SHOULD NOT be extracted for {extractor.name} extractions: {json.dumps(extractor.prompt_negative_examples)}", file=buffer)
|
73
|
+
print("</extractor>", file=buffer)
|
74
|
+
print("\n"*2, file=buffer)
|
75
|
+
|
76
|
+
logging.debug("======== extractors ======")
|
77
|
+
logging.debug(buffer.getvalue())
|
78
|
+
logging.debug("======== extractors end ======")
|
79
|
+
return buffer.getvalue()
|
80
|
+
|
81
|
+
|
82
|
+
|
83
|
+
if __name__ == '__main__':
|
84
|
+
a = ExtractionList(extractions=[Extraction(type="yes", id="1", value="2", original_text="3")], success=True)
|
85
|
+
print(a.model_dump())
|
txt2stix/attack_flow.py
ADDED
@@ -0,0 +1,101 @@
|
|
1
|
+
import logging
|
2
|
+
import uuid
|
3
|
+
from stix2 import Relationship
|
4
|
+
|
5
|
+
from txt2stix.common import UUID_NAMESPACE
|
6
|
+
from txt2stix.retriever import STIXObjectRetriever
|
7
|
+
from stix2extensions.attack_action import AttackAction, AttackFlow
|
8
|
+
from stix2extensions._extensions import attack_flow_ExtensionDefinitionSMO
|
9
|
+
from .utils import AttackFlowList
|
10
|
+
|
11
|
+
|
12
|
+
def parse_flow(report, flow: AttackFlowList):
|
13
|
+
logging.info(f"flow.success = {flow.success}")
|
14
|
+
if not flow.success:
|
15
|
+
return []
|
16
|
+
attack_objects = STIXObjectRetriever().get_attack_objects(
|
17
|
+
flow.matrix,
|
18
|
+
[item.attack_tactic_id for item in flow.items]
|
19
|
+
+ [item.attack_technique_id for item in flow.items],
|
20
|
+
)
|
21
|
+
attack_objects = {
|
22
|
+
obj["external_references"][0]["external_id"]: obj for obj in attack_objects
|
23
|
+
}
|
24
|
+
flow_objects = [report, attack_flow_ExtensionDefinitionSMO]
|
25
|
+
last_action = None
|
26
|
+
for i, item in enumerate(flow.items):
|
27
|
+
try:
|
28
|
+
tactic_obj = attack_objects[item.attack_tactic_id]
|
29
|
+
technique_obj = attack_objects[item.attack_technique_id]
|
30
|
+
action_obj = AttackAction(
|
31
|
+
**{
|
32
|
+
"id": flow_id(
|
33
|
+
report["id"], item.attack_technique_id, item.attack_tactic_id
|
34
|
+
),
|
35
|
+
"effect_refs": [f"attack-action--{str(uuid.uuid4())}"],
|
36
|
+
"technique_id": item.attack_technique_id,
|
37
|
+
"technique_ref": technique_obj["id"],
|
38
|
+
"tactic_id": item.attack_tactic_id,
|
39
|
+
"tactic_ref": tactic_obj["id"],
|
40
|
+
"name": item.name,
|
41
|
+
"description": item.description,
|
42
|
+
},
|
43
|
+
allow_custom=True,
|
44
|
+
)
|
45
|
+
action_obj.effect_refs.clear()
|
46
|
+
if i == 0:
|
47
|
+
flow_obj = {
|
48
|
+
"type": "attack-flow",
|
49
|
+
"id": report.id.replace("report", "attack-flow"),
|
50
|
+
"spec_version": "2.1",
|
51
|
+
"created": report.created,
|
52
|
+
"modified": report.modified,
|
53
|
+
"created_by_ref": report.created_by_ref,
|
54
|
+
"start_refs": [action_obj["id"]],
|
55
|
+
"name": report.name,
|
56
|
+
"description": report.description,
|
57
|
+
"scope": "malware",
|
58
|
+
"external_references": report.external_references,
|
59
|
+
"object_marking_refs": report.object_marking_refs,
|
60
|
+
}
|
61
|
+
flow_objects.append(AttackFlow(**flow_obj))
|
62
|
+
flow_objects.append(
|
63
|
+
Relationship(
|
64
|
+
type="relationship",
|
65
|
+
spec_version="2.1",
|
66
|
+
id="relationship--"
|
67
|
+
+ str(uuid.uuid5(UUID_NAMESPACE, f"attack-flow+{report.id}")),
|
68
|
+
created_by_ref=report.created_by_ref,
|
69
|
+
created=report.created,
|
70
|
+
modified=report.modified,
|
71
|
+
relationship_type="attack-flow",
|
72
|
+
description=f"Attack Flow for {report.name}",
|
73
|
+
source_ref=report.id,
|
74
|
+
target_ref=flow_obj["id"],
|
75
|
+
external_references=report.external_references,
|
76
|
+
object_marking_refs=report.object_marking_refs,
|
77
|
+
)
|
78
|
+
)
|
79
|
+
else:
|
80
|
+
last_action["effect_refs"].append(action_obj["id"])
|
81
|
+
flow_objects.append(tactic_obj)
|
82
|
+
flow_objects.append(technique_obj)
|
83
|
+
flow_objects.append(action_obj)
|
84
|
+
last_action = action_obj
|
85
|
+
except Exception as e:
|
86
|
+
if flow_objects == 2:
|
87
|
+
logging.exception("FATAL: create attack flow object failed")
|
88
|
+
return []
|
89
|
+
logging.debug("create attack-action failed", exc_info=True)
|
90
|
+
raise
|
91
|
+
|
92
|
+
return flow_objects
|
93
|
+
|
94
|
+
|
95
|
+
def flow_id(report_id, technique_id, tactic_id):
|
96
|
+
return "attack-action--" + str(
|
97
|
+
uuid.uuid5(
|
98
|
+
uuid.UUID(report_id.split("--")[-1]),
|
99
|
+
f"{report_id}+{technique_id}+{tactic_id}",
|
100
|
+
)
|
101
|
+
)
|