mirror of https://github.com/MISP/misp-modules
Merge branch 'main' of github.com:MISP/misp-modules
commit
e47252494c
|
@ -13,7 +13,7 @@ jobs:
|
|||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ["3.8", "3.9", "3.10", "3.11"]
|
||||
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
|
||||
|
||||
steps:
|
||||
- name: Install packages
|
||||
|
@ -28,9 +28,13 @@ jobs:
|
|||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install flake8 pytest
|
||||
pip install flake8 pytest
|
||||
pip install pipenv
|
||||
sed -i "s/python_version.*/python_version = \"${{ matrix.python-version }}\"/" Pipfile
|
||||
pipenv lock
|
||||
pipenv requirements > requirements.txt
|
||||
# pyfaul must be installed manually (?)
|
||||
pip install -r REQUIREMENTS pyfaup
|
||||
pip install -r requirements.txt pyfaup
|
||||
pip install .
|
||||
- name: Lint with flake8
|
||||
run: |
|
||||
|
|
|
@ -19,4 +19,6 @@ site*
|
|||
venv*
|
||||
|
||||
#vscode
|
||||
.vscode*
|
||||
.vscode*
|
||||
*.sqlite
|
||||
website/conf/config.cfg
|
||||
|
|
13
Pipfile
13
Pipfile
|
@ -10,6 +10,10 @@ pytest = "*"
|
|||
flake8 = "*"
|
||||
|
||||
[packages]
|
||||
numpy = "<2.0.0"
|
||||
matplotlib = "*"
|
||||
sigmf = "*"
|
||||
pysafebrowsing = "*"
|
||||
dnspython = "*"
|
||||
requests = { extras = ["security"], version = "*" }
|
||||
urlarchiver = "*"
|
||||
|
@ -50,8 +54,8 @@ ODTReader = { git = "https://github.com/cartertemm/ODTReader.git/" }
|
|||
python-pptx = "*"
|
||||
python-docx = "*"
|
||||
ezodf = "*"
|
||||
pandas = "==1.3.5"
|
||||
pandas_ods_reader = "==0.1.2"
|
||||
pandas = "*"
|
||||
pandas_ods_reader = "*"
|
||||
pdftotext = "*"
|
||||
lxml = "*"
|
||||
xlrd = "*"
|
||||
|
@ -73,9 +77,10 @@ crowdstrike-falconpy = "0.9.0"
|
|||
censys = "2.0.9"
|
||||
mwdblib = "3.4.1"
|
||||
ndjson = "0.3.1"
|
||||
Jinja2 = "3.1.2"
|
||||
Jinja2 = ">=3.1.2"
|
||||
mattermostdriver = "7.3.2"
|
||||
openpyxl = "*"
|
||||
slack-sdk = "3.27.1"
|
||||
|
||||
[requires]
|
||||
python_version = "3.7"
|
||||
python_version = "3.12"
|
||||
|
|
File diff suppressed because it is too large
Load Diff
16
README.md
16
README.md
|
@ -3,10 +3,12 @@
|
|||
[](https://github.com/MISP/misp-modules/actions/workflows/python-package.yml)[](https://coveralls.io/github/MISP/misp-modules?branch=main)
|
||||
[](https://codecov.io/gh/MISP/misp-modules)
|
||||
|
||||
MISP modules are autonomous modules that can be used to extend [MISP](https://github.com/MISP/MISP) for new services such as expansion, import and export.
|
||||
MISP modules are autonomous modules that can be used to extend [MISP](https://github.com/MISP/MISP) for new services such as expansion, import, export and workflow action.
|
||||
|
||||
MISP modules can be also installed and used without MISP as a [standalone tool accessible via a convenient web interface](./website).
|
||||
|
||||
The modules are written in Python 3 following a simple API interface. The objective is to ease the extensions of MISP functionalities
|
||||
without modifying core components. The API is available via a simple REST API which is independent from MISP installation or configuration.
|
||||
without modifying core components. The API is available via a simple REST API which is independent from MISP installation or configuration and can be used with other tools.
|
||||
|
||||
For more information: [Extending MISP with Python modules](https://www.misp-project.org/misp-training/3.1-misp-modules.pdf) slides from [MISP training](https://github.com/MISP/misp-training).
|
||||
|
||||
|
@ -43,8 +45,10 @@ For more information: [Extending MISP with Python modules](https://www.misp-proj
|
|||
* [GeoIP](misp_modules/modules/expansion/geoip_country.py) - a hover and expansion module to get GeoIP information from geolite/maxmind.
|
||||
* [GeoIP_City](misp_modules/modules/expansion/geoip_city.py) - a hover and expansion module to get GeoIP City information from geolite/maxmind.
|
||||
* [GeoIP_ASN](misp_modules/modules/expansion/geoip_asn.py) - a hover and expansion module to get GeoIP ASN information from geolite/maxmind.
|
||||
* [Google Threat Intelligence] (https://github.com/MISP/misp-modules/tree/main/misp_modules/modules/expansion/google_threat_intelligence.py) - An expansion module to have the observable's threat score assessed by Google Threat Intelligence.
|
||||
* [GreyNoise](misp_modules/modules/expansion/greynoise.py) - a hover and expansion module to get IP and CVE information from GreyNoise.
|
||||
* [hashdd](misp_modules/modules/expansion/hashdd.py) - a hover module to check file hashes against [hashdd.com](http://www.hashdd.com) including NSLR dataset.
|
||||
* [Hashlookup](misp_modules/modules/expansion/hashlookup.py) - An expansion module to enrich a file hash with hashlookup.circl.lu services (NSRL and other sources)
|
||||
* [hibp](misp_modules/modules/expansion/hibp.py) - a hover module to lookup against Have I Been Pwned?
|
||||
* [html_to_markdown](misp_modules/modules/expansion/html_to_markdown.py) - Simple HTML to markdown converter
|
||||
* [HYAS Insight](misp_modules/modules/expansion/hyasinsight.py) - a hover and expansion module to get information from [HYAS Insight](https://www.hyas.com/hyas-insight).
|
||||
|
@ -82,6 +86,7 @@ For more information: [Extending MISP with Python modules](https://www.misp-proj
|
|||
* [Socialscan](misp_modules/modules/expansion/socialscan.py) - a hover module to check if an email address or a username is used on different online platforms, using the [socialscan](https://github.com/iojw/socialscan) python library
|
||||
* [SophosLabs Intelix](misp_modules/modules/expansion/sophoslabs_intelix.py) - SophosLabs Intelix is an API for Threat Intelligence and Analysis (free tier available). [SophosLabs](https://aws.amazon.com/marketplace/pp/B07SLZPMCS)
|
||||
* [sourcecache](misp_modules/modules/expansion/sourcecache.py) - a module to cache a specific link from a MISP instance.
|
||||
* [stairwell](misp_modules/modules/expansion/stairwell.py) - an expansion module to enrich hash observables with the Stairwell API
|
||||
* [STIX2 pattern syntax validator](misp_modules/modules/expansion/stix2_pattern_syntax_validator.py) - a module to check a STIX2 pattern syntax.
|
||||
* [ThreatCrowd](misp_modules/modules/expansion/threatcrowd.py) - an expansion module for [ThreatCrowd](https://www.threatcrowd.org/).
|
||||
* [threatminer](misp_modules/modules/expansion/threatminer.py) - an expansion module to expand from [ThreatMiner](https://www.threatminer.org/).
|
||||
|
@ -89,13 +94,12 @@ For more information: [Extending MISP with Python modules](https://www.misp-proj
|
|||
* [urlhaus](misp_modules/modules/expansion/urlhaus.py) - Query urlhaus to get additional data about a domain, hash, hostname, ip or url.
|
||||
* [urlscan](misp_modules/modules/expansion/urlscan.py) - an expansion module to query [urlscan.io](https://urlscan.io).
|
||||
* [variotdbs](misp_modules/modules/expansion/variotdbs.py) - an expansion module to query the [VARIoT db](https://www.variotdbs.pl) API to get more information about a Vulnerability
|
||||
* [virustotal](misp_modules/modules/expansion/virustotal.py) - an expansion module to query the [VirusTotal](https://www.virustotal.com/gui/home) API with a high request rate limit required. (More details about the API: [here](https://developers.virustotal.com/reference))
|
||||
* [virustotal_public](misp_modules/modules/expansion/virustotal_public.py) - an expansion module to query the [VirusTotal](https://www.virustotal.com/gui/home) API with a public key and a low request rate limit. (More details about the API: [here](https://developers.virustotal.com/reference))
|
||||
* [virustotal](misp_modules/modules/expansion/virustotal.py) - an expansion module to query the [VirusTotal](https://www.virustotal.com/gui/home) API with a high request rate limit required. (More details about the API: [here](https://docs.virustotal.com/reference/overview))
|
||||
* [virustotal_public](misp_modules/modules/expansion/virustotal_public.py) - an expansion module to query the [VirusTotal](https://www.virustotal.com/gui/home) API with a public key and a low request rate limit. (More details about the API: [here](https://docs.virustotal.com/reference/overview))
|
||||
* [VMray](misp_modules/modules/expansion/vmray_submit.py) - a module to submit a sample to VMray.
|
||||
* [VMware NSX](misp_modules/modules/expansion/vmware_nsx.py) - a module to enrich a file or URL with VMware NSX Defender.
|
||||
* [VulnDB](misp_modules/modules/expansion/vulndb.py) - a module to query [VulnDB](https://www.riskbasedsecurity.com/).
|
||||
* [Vulners](misp_modules/modules/expansion/vulners.py) - an expansion module to expand information about CVEs using Vulners API.
|
||||
* [Vysion](misp_modules/modules/expansion/vysion.py) - an expansion module to add dark web intelligence using Vysion API.
|
||||
* [whois](misp_modules/modules/expansion/whois.py) - a module to query a local instance of [uwhois](https://github.com/rafiot/uwhoisd).
|
||||
* [whoisfreaks](misp_modules/modules/expansion/whoisfreaks.py) - An expansion module for [whoisfreaks](https://whoisfreaks.com/) that will provide an enriched analysis of the provided domain, including WHOIS and DNS information.
|
||||
* [wikidata](misp_modules/modules/expansion/wiki.py) - a [wikidata](https://www.wikidata.org) expansion module.
|
||||
|
@ -576,7 +580,7 @@ cd ../
|
|||
|
||||
## Documentation
|
||||
|
||||
In order to provide documentation about some modules that require specific input / output / configuration, the [doc](doc) directory contains detailed information about the general purpose, requirements, features, input and ouput of each of these modules:
|
||||
In order to provide documentation about some modules that require specific input / output / configuration, the [index.md](docs/index.md) file within the [docs](docs) directory contains detailed information about the general purpose, requirements, features, input and ouput of each of these modules:
|
||||
|
||||
- ***description** - quick description of the general purpose of the module, as the one given by the moduleinfo
|
||||
- **requirements** - special libraries needed to make the module work
|
||||
|
|
300
REQUIREMENTS
300
REQUIREMENTS
|
@ -1,189 +1,181 @@
|
|||
-i https://pypi.org/simple
|
||||
aiohttp==3.8.4
|
||||
aiosignal==1.3.1 ; python_version >= '3.7'
|
||||
aiohttp==3.9.5; python_version >= '3.8'
|
||||
aiosignal==1.3.1; python_version >= '3.7'
|
||||
antlr4-python3-runtime==4.9.3
|
||||
anyio==3.6.2 ; python_full_version >= '3.6.2'
|
||||
git+https://github.com/davidonzo/apiosintDS@misp
|
||||
anyio==4.4.0; python_version >= '3.8'
|
||||
apiosintds==2.0.3; python_version >= '3.6'
|
||||
appdirs==1.4.4
|
||||
argcomplete==3.0.8 ; python_version >= '3.6'
|
||||
argparse==1.4.0
|
||||
assemblyline-client==4.5.1
|
||||
async-timeout==4.0.2 ; python_version >= '3.6'
|
||||
asynctest==0.13.0 ; python_version < '3.8'
|
||||
attrs==23.1.0 ; python_version >= '3.7'
|
||||
backoff==2.2.1 ; python_version >= '3.7' and python_version < '4.0'
|
||||
backports.zoneinfo==0.2.1 ; python_version < '3.9'
|
||||
assemblyline-client==4.9.3
|
||||
attrs==23.2.0; python_version >= '3.7'
|
||||
backoff==1.11.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
|
||||
backscatter==0.2.4
|
||||
beautifulsoup4==4.12.2
|
||||
bidict==0.22.1 ; python_version >= '3.7'
|
||||
beautifulsoup4==4.12.3; python_full_version >= '3.6.0'
|
||||
bidict==0.23.1; python_version >= '3.8'
|
||||
blockchain==1.4.4
|
||||
censys==2.2.2
|
||||
certifi==2023.5.7 ; python_version >= '3.6'
|
||||
cffi==1.15.1
|
||||
chardet==5.1.0
|
||||
charset-normalizer==3.1.0 ; python_full_version >= '3.7.0'
|
||||
cattrs==23.2.3; python_version >= '3.8'
|
||||
censys==2.0.9; python_full_version >= '3.6.2' and python_version < '4.0'
|
||||
certifi==2024.7.4; python_version >= '3.6'
|
||||
cffi==1.16.0; platform_python_implementation != 'PyPy'
|
||||
chardet==5.2.0; python_version >= '3.7'
|
||||
charset-normalizer==3.3.2; python_full_version >= '3.7.0'
|
||||
clamd==1.0.2
|
||||
click==8.1.3 ; python_version >= '3.7'
|
||||
click==8.1.7; python_version >= '3.7'
|
||||
click-plugins==1.1.1
|
||||
colorama==0.4.6 ; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'
|
||||
colorclass==2.2.2 ; python_version >= '2.6'
|
||||
colorama==0.4.6; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'
|
||||
colorclass==2.2.2; python_version >= '2.6'
|
||||
commonmark==0.9.1
|
||||
compressed-rtf==1.0.6
|
||||
configparser==5.3.0 ; python_version >= '3.7'
|
||||
crowdstrike-falconpy==1.2.15
|
||||
cryptography==40.0.2 ; python_version >= '3.6'
|
||||
dateparser==1.1.8 ; python_version >= '3.7'
|
||||
decorator==5.1.1 ; python_version >= '3.5'
|
||||
deprecated==1.2.14 ; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
|
||||
configparser==7.0.0; python_version >= '3.8'
|
||||
contourpy==1.2.1; python_version >= '3.9'
|
||||
crowdstrike-falconpy==0.9.0; python_version >= '3.6'
|
||||
cryptography==42.0.8; python_version >= '3.7'
|
||||
cycler==0.12.1; python_version >= '3.8'
|
||||
deprecated==1.2.14; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
|
||||
dnsdb2==1.1.4
|
||||
dnspython==2.3.0
|
||||
domaintools-api==1.0.1
|
||||
dnspython==2.6.1; python_version >= '3.8'
|
||||
domaintools-api==2.0.0; python_version >= '3.6'
|
||||
easygui==0.98.3
|
||||
ebcdic==1.1.1
|
||||
enum-compat==0.0.3
|
||||
et-xmlfile==1.1.0 ; python_version >= '3.6'
|
||||
extract-msg==0.45.0
|
||||
et-xmlfile==1.1.0; python_version >= '3.6'
|
||||
extract-msg==0.48.7
|
||||
ezodf==0.3.2
|
||||
filelock==3.12.0 ; python_version >= '3.7'
|
||||
frozenlist==1.3.3 ; python_version >= '3.7'
|
||||
future==0.18.3 ; python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'
|
||||
geoip2==4.7.0
|
||||
h11==0.14.0 ; python_version >= '3.7'
|
||||
httpcore==0.17.1 ; python_version >= '3.7'
|
||||
httplib2==0.22.0 ; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
|
||||
httpx==0.24.1 ; python_version >= '3.7'
|
||||
idna==3.4 ; python_version >= '3.5'
|
||||
imapclient==2.3.1
|
||||
importlib-metadata==4.13.0 ; python_version < '3.8'
|
||||
importlib-resources==5.12.0 ; python_version < '3.9'
|
||||
filelock==3.15.4; python_version >= '3.8'
|
||||
fonttools==4.53.1; python_version >= '3.8'
|
||||
frozenlist==1.4.1; python_version >= '3.8'
|
||||
future==1.0.0; python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'
|
||||
geoip2==4.8.0; python_version >= '3.8'
|
||||
h11==0.14.0; python_version >= '3.7'
|
||||
httpcore==1.0.5; python_version >= '3.8'
|
||||
httplib2==0.22.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
|
||||
httpx==0.27.0; python_version >= '3.8'
|
||||
idna==3.7; python_version >= '3.5'
|
||||
isodate==0.6.1
|
||||
itsdangerous==2.1.2 ; python_version >= '3.7'
|
||||
jaraco.classes==3.2.3 ; python_version >= '3.7'
|
||||
jbxapi==3.21.0
|
||||
jeepney==0.8.0 ; sys_platform == 'linux'
|
||||
jinja2==3.1.2
|
||||
json-log-formatter==0.5.2 ; python_version >= '2.7'
|
||||
jsonschema==4.19.0 ; python_version >= '3.7'
|
||||
keyring==23.13.1 ; python_version >= '3.7'
|
||||
lark-parser==0.12.0
|
||||
lief==0.13.2
|
||||
lxml==4.9.2
|
||||
jbxapi==3.23.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
|
||||
jinja2==3.1.2; python_version >= '3.7'
|
||||
json-log-formatter==1.0; python_version >= '3.6'
|
||||
jsonschema==4.23.0; python_version >= '3.8'
|
||||
jsonschema-specifications==2023.12.1; python_version >= '3.8'
|
||||
kiwisolver==1.4.5; python_version >= '3.7'
|
||||
lark==1.1.9; python_version >= '3.6'
|
||||
lief==0.14.1
|
||||
lxml==5.2.2; python_version >= '3.6'
|
||||
maclookup==1.0.3
|
||||
markdown-it-py==2.2.0 ; python_version >= '3.7'
|
||||
markdownify==0.5.3
|
||||
markupsafe==2.1.2 ; python_version >= '3.7'
|
||||
matplotlib==3.7.2 ; python_version >= '3.8'
|
||||
matplotlib==3.5.3 ; python_version == '3.7'
|
||||
mattermostdriver==7.3.2
|
||||
maxminddb==2.3.0 ; python_version >= '3.7'
|
||||
mdurl==0.1.2 ; python_version >= '3.7'
|
||||
.
|
||||
more-itertools==9.1.0 ; python_version >= '3.7'
|
||||
msoffcrypto-tool==5.0.1 ; python_version >= '3' and platform_python_implementation != 'PyPy' or (platform_system != 'Windows' and platform_system != 'Darwin')
|
||||
multidict==6.0.4 ; python_version >= '3.7'
|
||||
mwdblib==4.4.0
|
||||
markupsafe==2.1.5; python_version >= '3.7'
|
||||
matplotlib==3.9.1; python_version >= '3.9'
|
||||
mattermostdriver==7.3.2; python_version >= '3.5'
|
||||
maxminddb==2.6.2; python_version >= '3.8'
|
||||
-e .
|
||||
more-itertools==10.3.0; python_version >= '3.8'
|
||||
msoffcrypto-tool==5.4.1; platform_python_implementation != 'PyPy' or (python_version >= '3' and platform_system != 'Windows' and platform_system != 'Darwin')
|
||||
multidict==6.0.5; python_version >= '3.7'
|
||||
mwdblib==3.4.1
|
||||
ndjson==0.3.1
|
||||
np==1.0.2
|
||||
numpy==1.21.6 ; python_version < '3.10' and platform_machine == 'aarch64'
|
||||
numpy==1.26.4; python_version >= '3.9'
|
||||
oauth2==1.9.0.post1
|
||||
git+https://github.com/cartertemm/ODTReader.git/@49d6938693f6faa3ff09998f86dba551ae3a996b#egg=odtreader
|
||||
olefile==0.46 ; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
|
||||
oletools==0.60.1
|
||||
opencv-python==4.7.0.72
|
||||
openpyxl==3.1.2
|
||||
packaging==23.1 ; python_version >= '3.7'
|
||||
pandas==1.5.3
|
||||
pandas-ods-reader==0.1.4
|
||||
odtreader@ git+https://github.com/cartertemm/ODTReader.git/@49d6938693f6faa3ff09998f86dba551ae3a996b
|
||||
olefile==0.47; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
|
||||
oletools==0.60.2
|
||||
opencv-python==4.10.0.84; python_version >= '3.6'
|
||||
openpyxl==3.1.5; python_version >= '3.8'
|
||||
packaging==24.1; python_version >= '3.8'
|
||||
pandas==1.3.5; python_full_version >= '3.7.1'
|
||||
pandas-ods-reader==0.1.2
|
||||
passivetotal==2.5.9
|
||||
pcodedmp==1.2.6
|
||||
pdftotext==2.2.2
|
||||
pillow==9.5.0
|
||||
pkgutil-resolve-name==1.3.10 ; python_version < '3.9'
|
||||
progressbar2==4.2.0 ; python_full_version >= '3.7.0'
|
||||
psutil==5.9.5 ; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
|
||||
publicsuffixlist==0.10.0.20230828 ; python_version >= '2.6'
|
||||
git+https://github.com/D4-project/BGP-Ranking.git/@68de39f6c5196f796055c1ac34504054d688aa59#egg=pybgpranking&subdirectory=client
|
||||
pycountry==22.3.5
|
||||
pycparser==2.21
|
||||
pycryptodome==3.18.0 ; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
|
||||
pycryptodomex==3.17 ; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
|
||||
pillow==10.4.0; python_version >= '3.8'
|
||||
platformdirs==4.2.2; python_version >= '3.8'
|
||||
progressbar2==4.4.2; python_version >= '3.8'
|
||||
psutil==6.0.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'
|
||||
publicsuffixlist==1.0.1.20240702; python_version >= '3.5'
|
||||
pybgpranking@ git+https://github.com/D4-project/BGP-Ranking.git/@68de39f6c5196f796055c1ac34504054d688aa59#subdirectory=client
|
||||
pycountry==22.3.5; python_version >= '3.6' and python_version < '4'
|
||||
pycparser==2.22; python_version >= '3.8'
|
||||
pycryptodome==3.20.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
|
||||
pycryptodomex==3.20.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
|
||||
pydeep2==0.5.1
|
||||
git+https://github.com/sebdraven/pydnstrails@48c1f740025c51289f43a24863d1845ff12fd21a#egg=pydnstrails
|
||||
pyeupi==1.1
|
||||
pydnstrails@ git+https://github.com/sebdraven/pydnstrails@48c1f740025c51289f43a24863d1845ff12fd21a
|
||||
pyeupi==1.3.0; python_version >= '3.8' and python_version < '4.0'
|
||||
pyfaup==1.2
|
||||
pygeoip==0.3.2
|
||||
pygments==2.15.1 ; python_version >= '3.7'
|
||||
git+https://github.com/MISP/PyIntel471.git@917272fafa8e12102329faca52173e90c5256968#egg=pyintel471
|
||||
git+https://github.com/D4-project/IPASN-History.git/@a2853c39265cecdd0c0d16850bd34621c0551b87#egg=pyipasnhistory&subdirectory=client
|
||||
pymisp[email,fileobjects,openioc,pdfexport,url]==2.4.175
|
||||
git+https://github.com/sebdraven/pyonyphe@d1d6741f8ea4475f3bb77ff20c876f08839cabd1#egg=pyonyphe
|
||||
pyparsing==2.4.7 ; python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'
|
||||
pypdns==1.5.2
|
||||
pypssl==2.2
|
||||
pyrsistent==0.19.3 ; python_version >= '3.7'
|
||||
pysafebrowsing==0.1.2
|
||||
pytesseract==0.3.10
|
||||
pygments==2.18.0; python_version >= '3.8'
|
||||
pyintel471@ git+https://github.com/MISP/PyIntel471.git@917272fafa8e12102329faca52173e90c5256968
|
||||
pyipasnhistory@ git+https://github.com/D4-project/IPASN-History.git/@a2853c39265cecdd0c0d16850bd34621c0551b87#subdirectory=client
|
||||
pymisp[fileobjects,openioc,pdfexport,email,url]==2.4.194; python_version >= '3.8' and python_version < '4.0'
|
||||
pyonyphe@ git+https://github.com/sebdraven/pyonyphe@d1d6741f8ea4475f3bb77ff20c876f08839cabd1
|
||||
pyparsing==2.4.7; python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'
|
||||
pypdns==2.2.3; python_version >= '3.8' and python_version < '4.0'
|
||||
pypssl==2.2; python_version >= '3.6' and python_version < '4.0'
|
||||
pysafebrowsing==0.1.3
|
||||
pytesseract==0.3.10; python_version >= '3.7'
|
||||
python-baseconv==1.2.2
|
||||
python-dateutil==2.8.2 ; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
|
||||
python-docx==0.8.11
|
||||
python-engineio==4.4.1 ; python_version >= '3.6'
|
||||
python-dateutil==2.9.0.post0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'
|
||||
python-docx==1.1.2; python_version >= '3.7'
|
||||
python-engineio==4.9.1; python_version >= '3.6'
|
||||
python-magic==0.4.27
|
||||
python-pptx==0.6.21
|
||||
python-socketio[client]==5.8.0 ; python_version >= '3.6'
|
||||
python-utils==3.5.2 ; python_version >= '3.7'
|
||||
pytz==2023.3
|
||||
pytz-deprecation-shim==0.1.0.post0 ; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'
|
||||
pyyaml==6.0 ; python_version >= '3.6'
|
||||
python-pptx==0.6.23
|
||||
python-socketio[client]==5.11.3; python_version >= '3.8'
|
||||
python-utils==3.8.2; python_version >= '3.9'
|
||||
pytz==2024.1
|
||||
pyyaml==6.0.1; python_version >= '3.6'
|
||||
pyzbar==0.1.9
|
||||
pyzipper==0.3.6 ; python_version >= '3.5'
|
||||
rdflib==6.3.2 ; python_version >= '3.7' and python_version < '4.0'
|
||||
pyzipper==0.3.6; python_version >= '3.5'
|
||||
rdflib==7.0.0; python_full_version >= '3.8.1' and python_full_version < '4.0.0'
|
||||
red-black-tree-mod==1.20
|
||||
redis==4.5.5 ; python_version >= '3.7'
|
||||
regex==2023.5.5 ; python_version >= '3.6'
|
||||
reportlab==4.0.4
|
||||
requests[security]==2.31.0
|
||||
requests-cache==0.6.4 ; python_version >= '3.6'
|
||||
requests-file==1.5.1
|
||||
rich==13.3.5 ; python_full_version >= '3.7.0'
|
||||
rtfde==0.1.0
|
||||
secretstorage==3.3.3 ; sys_platform == 'linux'
|
||||
setuptools==67.7.2 ; python_version >= '3.7'
|
||||
shodan==1.29.1
|
||||
sigmatools==0.19.1
|
||||
sigmf==1.1.1
|
||||
simplejson==3.19.1 ; python_version >= '2.5' and python_version not in '3.0, 3.1, 3.2, 3.3'
|
||||
six==1.16.0 ; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
|
||||
sniffio==1.3.0 ; python_version >= '3.7'
|
||||
socialscan==1.4
|
||||
redis==5.0.7; python_version >= '3.7'
|
||||
referencing==0.35.1; python_version >= '3.8'
|
||||
reportlab==4.2.2; python_version >= '3.7' and python_version < '4'
|
||||
requests[security]==2.32.3; python_version >= '3.8'
|
||||
requests-cache==1.2.1; python_version >= '3.8'
|
||||
requests-file==2.1.0
|
||||
rich==10.16.2; python_full_version >= '3.6.2' and python_full_version < '4.0.0'
|
||||
rpds-py==0.19.0; python_version >= '3.8'
|
||||
rtfde==0.1.2
|
||||
ruamel.yaml==0.18.6; python_version >= '3.7'
|
||||
ruamel.yaml.clib==0.2.8; platform_python_implementation == 'CPython' and python_version < '3.13'
|
||||
setuptools==70.3.0; python_version >= '3.8'
|
||||
shellingham==1.5.4; python_version >= '3.7'
|
||||
shodan==1.31.0
|
||||
sigmatools==0.23.1; python_version ~= '3.8'
|
||||
sigmf==1.2.2; python_version >= '3.7'
|
||||
simple-websocket==1.0.0; python_version >= '3.6'
|
||||
simplejson==3.19.2; python_version >= '2.5' and python_version not in '3.0, 3.1, 3.2'
|
||||
six==1.16.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'
|
||||
slack-sdk==3.27.1; python_version >= '3.6'
|
||||
sniffio==1.3.1; python_version >= '3.7'
|
||||
socialscan==1.4.0; python_version >= '3.6'
|
||||
socketio-client==0.5.7.4
|
||||
soupsieve==2.4.1 ; python_version >= '3.7'
|
||||
sparqlwrapper==2.0.0
|
||||
stix2==3.0.1
|
||||
stix2-patterns==2.0.0
|
||||
tabulate==0.9.0 ; python_version >= '3.7'
|
||||
tau-clients==0.3.0
|
||||
soupsieve==2.5; python_version >= '3.8'
|
||||
sparqlwrapper==2.0.0; python_version >= '3.7'
|
||||
stix2==3.0.1; python_version >= '3.6'
|
||||
stix2-patterns==2.0.0; python_version >= '3.6'
|
||||
tabulate==0.9.0; python_version >= '3.7'
|
||||
tau-clients==0.3.0; python_version >= '3.6'
|
||||
taxii2-client==2.3.0
|
||||
tldextract==3.4.3 ; python_version >= '3.7'
|
||||
tornado==6.2 ; python_version >= '3.7'
|
||||
tqdm==4.65.0 ; python_version >= '3.7'
|
||||
git+https://github.com/SteveClement/trustar-python.git@6954eae38e0c77eaeef26084b6c5fd033925c1c7#egg=trustar
|
||||
typing-extensions==4.5.0 ; python_version < '3.8'
|
||||
tzdata==2023.3 ; python_version >= '3.6'
|
||||
tzlocal==4.2 ; python_version >= '3.6'
|
||||
termcolor==2.4.0; python_version >= '3.8'
|
||||
tldextract==5.1.2; python_version >= '3.8'
|
||||
tornado==6.4.1; python_version >= '3.8'
|
||||
tqdm==4.66.4; python_version >= '3.7'
|
||||
trustar@ git+https://github.com/SteveClement/trustar-python.git@6954eae38e0c77eaeef26084b6c5fd033925c1c7
|
||||
typer==0.12.3; python_version >= '3.7'
|
||||
typing-extensions==4.12.2; python_version >= '3.8'
|
||||
tzlocal==5.2; python_version >= '3.8'
|
||||
unicodecsv==0.14.1
|
||||
url-normalize==1.4.3 ; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'
|
||||
url-normalize==1.4.3; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'
|
||||
urlarchiver==0.2
|
||||
urllib3==1.26.15 ; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'
|
||||
validators==0.14.0
|
||||
urllib3==2.2.2; python_version >= '3.8'
|
||||
vt-graph-api==2.2.0
|
||||
vt-py==0.17.5
|
||||
vulners==2.0.10
|
||||
vysion==1.0.10
|
||||
wand==0.6.11
|
||||
websocket-client==1.5.1 ; python_version >= '3.7'
|
||||
websockets==11.0.3 ; python_version >= '3.7'
|
||||
wrapt==1.15.0 ; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
|
||||
xlrd==2.0.1
|
||||
xlsxwriter==3.1.0 ; python_version >= '3.6'
|
||||
vt-py==0.18.2; python_full_version >= '3.7.0'
|
||||
vulners==2.1.7; python_version >= '3.8'
|
||||
wand==0.6.13
|
||||
websocket-client==1.8.0; python_version >= '3.8'
|
||||
websockets==12.0; python_version >= '3.8'
|
||||
wrapt==1.16.0; python_version >= '3.6'
|
||||
wsproto==1.2.0; python_full_version >= '3.7.0'
|
||||
xlrd==2.0.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'
|
||||
xlsxwriter==3.2.0; python_version >= '3.6'
|
||||
yara-python==3.8.1
|
||||
yarl==1.9.2 ; python_version >= '3.7'
|
||||
zipp==3.15.0 ; python_version >= '3.7'
|
||||
yarl==1.9.4; python_version >= '3.7'
|
||||
|
|
|
@ -38,6 +38,7 @@ For more information: [Extending MISP with Python modules](https://www.circl.lu/
|
|||
* [EQL](misp_modules/modules/expansion/eql.py) - an expansion module to generate event query language (EQL) from an attribute. [Event Query Language](https://eql.readthedocs.io/en/latest/)
|
||||
* [Farsight DNSDB Passive DNS](https://github.com/MISP/misp-modules/tree/main/misp_modules/modules/expansion/farsight_passivedns.py) - a hover and expansion module to expand hostname and IP addresses with passive DNS information.
|
||||
* [GeoIP](https://github.com/MISP/misp-modules/tree/main/misp_modules/modules/expansion/geoip_country.py) - a hover and expansion module to get GeoIP information from geolite/maxmind.
|
||||
* [Google Threat Intelligence] (https://github.com/MISP/misp-modules/tree/main/misp_modules/modules/expansion/google_threat_intelligence.py) - An expansion module to have the observable's threat score assessed by Google Threat Intelligence.
|
||||
* [Greynoise](https://github.com/MISP/misp-modules/tree/main/misp_modules/modules/expansion/greynoise.py) - a hover to get information from greynoise.
|
||||
* [hashdd](https://github.com/MISP/misp-modules/tree/main/misp_modules/modules/expansion/hashdd.py) - a hover module to check file hashes against [hashdd.com](http://www.hashdd.com) including NSLR dataset.
|
||||
* [hibp](https://github.com/MISP/misp-modules/tree/main/misp_modules/modules/expansion/hibp.py) - a hover module to lookup against Have I Been Pwned?
|
||||
|
@ -70,12 +71,11 @@ For more information: [Extending MISP with Python modules](https://www.circl.lu/
|
|||
* [threatminer](https://github.com/MISP/misp-modules/tree/main/misp_modules/modules/expansion/threatminer.py) - an expansion module to expand from [ThreatMiner](https://www.threatminer.org/).
|
||||
* [urlhaus](https://github.com/MISP/misp-modules/tree/main/misp_modules/modules/expansion/urlhaus.py) - Query urlhaus to get additional data about a domain, hash, hostname, ip or url.
|
||||
* [urlscan](https://github.com/MISP/misp-modules/tree/main/misp_modules/modules/expansion/urlscan.py) - an expansion module to query [urlscan.io](https://urlscan.io).
|
||||
* [virustotal](https://github.com/MISP/misp-modules/tree/main/misp_modules/modules/expansion/virustotal.py) - an expansion module to query the [VirusTotal](https://www.virustotal.com/gui/home) API with a high request rate limit required. (More details about the API: [here](https://developers.virustotal.com/reference))
|
||||
* [virustotal_public](https://github.com/MISP/misp-modules/tree/main/misp_modules/modules/expansion/virustotal_public.py) - an expansion module to query the [VirusTotal](https://www.virustotal.com/gui/home) API with a public key and a low request rate limit. (More details about the API: [here](https://developers.virustotal.com/reference))
|
||||
* [virustotal](https://github.com/MISP/misp-modules/tree/main/misp_modules/modules/expansion/virustotal.py) - an expansion module to query the [VirusTotal](https://www.virustotal.com/gui/home) API with a high request rate limit required. (More details about the API: [here](https://docs.virustotal.com/reference/overview))
|
||||
* [virustotal_public](https://github.com/MISP/misp-modules/tree/main/misp_modules/modules/expansion/virustotal_public.py) - an expansion module to query the [VirusTotal](https://www.virustotal.com/gui/home) API with a public key and a low request rate limit. (More details about the API: [here](https://docs.virustotal.com/reference/overview))
|
||||
* [VMray](https://github.com/MISP/misp-modules/tree/main/misp_modules/modules/expansion/vmray_submit.py) - a module to submit a sample to VMray.
|
||||
* [VulnDB](https://github.com/MISP/misp-modules/tree/main/misp_modules/modules/expansion/vulndb.py) - a module to query [VulnDB](https://www.riskbasedsecurity.com/).
|
||||
* [Vulners](https://github.com/MISP/misp-modules/tree/main/misp_modules/modules/expansion/vulners.py) - an expansion module to expand information about CVEs using Vulners API.
|
||||
* [Vysion](https://github.com/MISP/misp-modules/tree/main/misp_modules/modules/expansion/vysion.py) - an expansion module to add dark web intelligence using Vysion API.
|
||||
* [whois](https://github.com/MISP/misp-modules/tree/main/misp_modules/modules/expansion/whois.py) - a module to query a local instance of [uwhois](https://github.com/rafiot/uwhoisd).
|
||||
* [wikidata](https://github.com/MISP/misp-modules/tree/main/misp_modules/modules/expansion/wiki.py) - a [wikidata](https://www.wikidata.org) expansion module.
|
||||
* [xforce](https://github.com/MISP/misp-modules/tree/main/misp_modules/modules/expansion/xforceexchange.py) - an IBM X-Force Exchange expansion module.
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 18 KiB |
Binary file not shown.
After Width: | Height: | Size: 9.0 KiB |
Binary file not shown.
After Width: | Height: | Size: 32 KiB |
|
@ -655,6 +655,27 @@ Module to query a local copy of Maxmind's Geolite database.
|
|||
|
||||
-----
|
||||
|
||||
#### [google_threat_intelligence](https://github.com/MISP/misp-modules/tree/main/misp_modules/modules/expansion/google_threat_intelligence.py)
|
||||
|
||||
<img src=logos/google_threat_intelligence.png height=60>
|
||||
|
||||
An expansion module to have the observable's threat score assessed by Google Threat Intelligence.
|
||||
- **features**:
|
||||
>GTI assessment for the given observable, this include information about level of severity, a clear verdict (malicious, suspicious, undetected and bening) and additional information provided by the Mandiant expertise combined with the VirusTotal database.
|
||||
>
|
||||
>[Output example screeshot](https://github.com/MISP/MISP/assets/4747608/e275db2f-bb1e-4413-8cc0-ec3cb05e0414)
|
||||
- **input**:
|
||||
>A domain, hash (md5, sha1, sha256 or sha512), hostname or IP address attribute.
|
||||
- **output**:
|
||||
>Text fields containing the threat score, the severity, the verdict and the threat label of the observable inspected.
|
||||
- **references**:
|
||||
> - https://www.virustotal.com/
|
||||
> - https://gtidocs.virustotal.com/reference
|
||||
- **requirements**:
|
||||
>An access to the Google Threat Intelligence API (apikey), with a high request rate limit.
|
||||
|
||||
-----
|
||||
|
||||
#### [greynoise](https://github.com/MISP/misp-modules/tree/main/misp_modules/modules/expansion/greynoise.py)
|
||||
|
||||
<img src=logos/greynoise.png height=60>
|
||||
|
@ -1582,6 +1603,25 @@ Module to cache web pages of analysis reports, OSINT sources. The module returns
|
|||
|
||||
-----
|
||||
|
||||
#### [stairwell](https://github.com/MISP/misp-modules/tree/main/misp_modules/modules/expansion/stairwell.py)
|
||||
|
||||
<img src=logos/stairwell.png height=60>
|
||||
|
||||
Module to query the Stairwell API to get additional information about the input hash attribute
|
||||
- **features**:
|
||||
>The module takes a hash attribute as input and queries Stariwell's API to fetch additional data about it. The result, if the payload is observed in Stariwell, is a file object describing the file the input hash is related to.
|
||||
- **input**:
|
||||
>A hash attribute (md5, sha1, sha256).
|
||||
- **output**:
|
||||
>File object related to the input attribute found on Stairwell platform.
|
||||
- **references**:
|
||||
> - https://stairwell.com
|
||||
> - https://docs.stairwell.com
|
||||
- **requirements**:
|
||||
>Access to Stairwell platform (apikey)
|
||||
|
||||
-----
|
||||
|
||||
#### [stix2_pattern_syntax_validator](https://github.com/MISP/misp-modules/tree/main/misp_modules/modules/expansion/stix2_pattern_syntax_validator.py)
|
||||
|
||||
<img src=logos/stix.png height=60>
|
||||
|
@ -1787,7 +1827,7 @@ Module to get advanced information from virustotal.
|
|||
>MISP attributes and objects resulting from the parsing of the VirusTotal report concerning the input attribute.
|
||||
- **references**:
|
||||
> - https://www.virustotal.com/
|
||||
> - https://developers.virustotal.com/reference
|
||||
> - https://docs.virustotal.com/reference/overview
|
||||
- **requirements**:
|
||||
>An access to the VirusTotal API (apikey), with a high request rate limit.
|
||||
|
||||
|
@ -1812,7 +1852,7 @@ Module to get information from VirusTotal.
|
|||
>MISP attributes and objects resulting from the parsing of the VirusTotal report concerning the input attribute.
|
||||
- **references**:
|
||||
> - https://www.virustotal.com
|
||||
> - https://developers.virustotal.com/reference
|
||||
> - https://docs.virustotal.com/reference/overview
|
||||
- **requirements**:
|
||||
>An access to the VirusTotal API (apikey)
|
||||
|
||||
|
@ -1904,26 +1944,6 @@ An expansion hover module to expand information about CVE id using Vulners API.
|
|||
|
||||
-----
|
||||
|
||||
#### [Vysion](https://github.com/MISP/misp-modules/tree/main/misp_modules/modules/expansion/vysion.py)
|
||||
|
||||
<img src=logos/vysion.png height=60>
|
||||
|
||||
Module to enrich the information by making use of the Vysion API.
|
||||
- **features**:
|
||||
>This module gets correlated information from our dark web intelligence database. With this you will get several objects containing information related to, for example, an organization victim of a ransomware attack.
|
||||
>MISP objects containing title, link to our webapp and TOR, i2p or clearnet URLs.
|
||||
- **input**:
|
||||
>MISP Attribute which include: company(target-org), country, info.
|
||||
- **output**:
|
||||
>MISP objects containing title, link to our webapp and TOR, i2p or clearnet URLs.
|
||||
- **references**:
|
||||
>https://vysion.ai/
|
||||
- **requirements**:
|
||||
> Vysion python library
|
||||
> Vysion API Key
|
||||
|
||||
-----
|
||||
|
||||
#### [whois](https://github.com/MISP/misp-modules/tree/main/misp_modules/modules/expansion/whois.py)
|
||||
|
||||
Module to query a local instance of uwhois (https://github.com/rafiot/uwhoisd).
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 9.0 KiB |
|
@ -652,6 +652,27 @@ Module to query a local copy of Maxmind's Geolite database.
|
|||
|
||||
-----
|
||||
|
||||
#### [google_threat_intelligence](https://github.com/MISP/misp-modules/tree/main/misp_modules/modules/expansion/google_threat_intelligence.py)
|
||||
|
||||
<img src=../logos/google_threat_intelligence.png height=60>
|
||||
|
||||
An expansion module to have the observable's threat score assessed by Google Threat Intelligence.
|
||||
- **features**:
|
||||
>GTI assessment for the given observable, this include information about level of severity, a clear verdict (malicious, suspicious, undetected and bening) and additional information provided by the Mandiant expertise combined with the VirusTotal database.
|
||||
>
|
||||
>[Output example screeshot](https://github.com/MISP/MISP/assets/4747608/e275db2f-bb1e-4413-8cc0-ec3cb05e0414)
|
||||
- **input**:
|
||||
>A domain, hash (md5, sha1, sha256 or sha512), hostname or IP address attribute.
|
||||
- **output**:
|
||||
>Text fields containing the threat score, the severity, the verdict and the threat label of the observable inspected.
|
||||
- **references**:
|
||||
> - https://www.virustotal.com/
|
||||
> - https://gtidocs.virustotal.com/reference
|
||||
- **requirements**:
|
||||
>An access to the Google Threat Intelligence API (apikey), with a high request rate limit.
|
||||
|
||||
-----
|
||||
|
||||
#### [greynoise](https://github.com/MISP/misp-modules/tree/main/misp_modules/modules/expansion/greynoise.py)
|
||||
|
||||
<img src=../logos/greynoise.png height=60>
|
||||
|
@ -1579,6 +1600,25 @@ Module to cache web pages of analysis reports, OSINT sources. The module returns
|
|||
|
||||
-----
|
||||
|
||||
#### [stairwell](https://github.com/MISP/misp-modules/tree/main/misp_modules/modules/expansion/stairwell.py)
|
||||
|
||||
<img src=../logos/stairwell.png height=60>
|
||||
|
||||
Module to query the Stairwell API to get additional information about the input hash attribute
|
||||
- **features**:
|
||||
>The module takes a hash attribute as input and queries Stariwell's API to fetch additional data about it. The result, if the payload is observed in Stariwell, is a file object describing the file the input hash is related to.
|
||||
- **input**:
|
||||
>A hash attribute (md5, sha1, sha256).
|
||||
- **output**:
|
||||
>File object related to the input attribute found on Stairwell platform.
|
||||
- **references**:
|
||||
> - https://stairwell.com
|
||||
> - https://docs.stairwell.com
|
||||
- **requirements**:
|
||||
>Access to Stairwell platform (apikey)
|
||||
|
||||
-----
|
||||
|
||||
#### [stix2_pattern_syntax_validator](https://github.com/MISP/misp-modules/tree/main/misp_modules/modules/expansion/stix2_pattern_syntax_validator.py)
|
||||
|
||||
<img src=../logos/stix.png height=60>
|
||||
|
@ -1784,7 +1824,7 @@ Module to get advanced information from virustotal.
|
|||
>MISP attributes and objects resulting from the parsing of the VirusTotal report concerning the input attribute.
|
||||
- **references**:
|
||||
> - https://www.virustotal.com/
|
||||
> - https://developers.virustotal.com/reference
|
||||
> - https://docs.virustotal.com/reference/overview
|
||||
- **requirements**:
|
||||
>An access to the VirusTotal API (apikey), with a high request rate limit.
|
||||
|
||||
|
@ -1809,7 +1849,7 @@ Module to get information from VirusTotal.
|
|||
>MISP attributes and objects resulting from the parsing of the VirusTotal report concerning the input attribute.
|
||||
- **references**:
|
||||
> - https://www.virustotal.com
|
||||
> - https://developers.virustotal.com/reference
|
||||
> - https://docs.virustotal.com/reference/overview
|
||||
- **requirements**:
|
||||
>An access to the VirusTotal API (apikey)
|
||||
|
||||
|
|
|
@ -70,8 +70,8 @@ For more information: [Extending MISP with Python modules](https://www.circl.lu/
|
|||
* [threatminer](https://github.com/MISP/misp-modules/tree/main/misp_modules/modules/expansion/threatminer.py) - an expansion module to expand from [ThreatMiner](https://www.threatminer.org/).
|
||||
* [urlhaus](https://github.com/MISP/misp-modules/tree/main/misp_modules/modules/expansion/urlhaus.py) - Query urlhaus to get additional data about a domain, hash, hostname, ip or url.
|
||||
* [urlscan](https://github.com/MISP/misp-modules/tree/main/misp_modules/modules/expansion/urlscan.py) - an expansion module to query [urlscan.io](https://urlscan.io).
|
||||
* [virustotal](https://github.com/MISP/misp-modules/tree/main/misp_modules/modules/expansion/virustotal.py) - an expansion module to query the [VirusTotal](https://www.virustotal.com/gui/home) API with a high request rate limit required. (More details about the API: [here](https://developers.virustotal.com/reference))
|
||||
* [virustotal_public](https://github.com/MISP/misp-modules/tree/main/misp_modules/modules/expansion/virustotal_public.py) - an expansion module to query the [VirusTotal](https://www.virustotal.com/gui/home) API with a public key and a low request rate limit. (More details about the API: [here](https://developers.virustotal.com/reference))
|
||||
* [virustotal](https://github.com/MISP/misp-modules/tree/main/misp_modules/modules/expansion/virustotal.py) - an expansion module to query the [VirusTotal](https://www.virustotal.com/gui/home) API with a high request rate limit required. (More details about the API: [here](https://docs.virustotal.com/reference/overview))
|
||||
* [virustotal_public](https://github.com/MISP/misp-modules/tree/main/misp_modules/modules/expansion/virustotal_public.py) - an expansion module to query the [VirusTotal](https://www.virustotal.com/gui/home) API with a public key and a low request rate limit. (More details about the API: [here](https://docs.virustotal.com/reference/overview))
|
||||
* [VMray](https://github.com/MISP/misp-modules/tree/main/misp_modules/modules/expansion/vmray_submit.py) - a module to submit a sample to VMray.
|
||||
* [VulnDB](https://github.com/MISP/misp-modules/tree/main/misp_modules/modules/expansion/vulndb.py) - a module to query [VulnDB](https://www.riskbasedsecurity.com/).
|
||||
* [Vulners](https://github.com/MISP/misp-modules/tree/main/misp_modules/modules/expansion/vulners.py) - an expansion module to expand information about CVEs using Vulners API.
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"description": "An expansion module to have the observable's threat score assessed by Google Threat Intelligence.",
|
||||
"logo": "google_threat_intelligence.png",
|
||||
"requirements": [
|
||||
"An access to the Google Threat Intelligence API (apikey), with a high request rate limit."
|
||||
],
|
||||
"input": "A domain, hash (md5, sha1, sha256 or sha512), hostname or IP address attribute.",
|
||||
"output": "Text fields containing the threat score, the severity, the verdict and the threat label of the observable inspected.",
|
||||
"references": [
|
||||
"https://www.virustotal.com/",
|
||||
"https://gtidocs.virustotal.com/reference"
|
||||
],
|
||||
"features": "GTI assessment for the given observable, this include information about level of severity, a clear verdict (malicious, suspicious, undetected and benign) and additional information provided by the Mandiant expertise combined with the VirusTotal database.\n\n[Output example screeshot](https://github.com/MISP/MISP/assets/4747608/e275db2f-bb1e-4413-8cc0-ec3cb05e0414)"
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"description": "Module to query the Stairwell API to get additional information about the input hash attribute",
|
||||
"logo": "stairwell.png",
|
||||
"requirements": [
|
||||
"Access to Stairwell platform (apikey)"
|
||||
],
|
||||
"input": "A hash attribute (md5, sha1, sha256).",
|
||||
"output": "File object related to the input attribute found on Stairwell platform.",
|
||||
"references": [
|
||||
"https://stairwell.com",
|
||||
"https://docs.stairwell.com"
|
||||
],
|
||||
"features": "The module takes a hash attribute as input and queries Stariwell's API to fetch additional data about it. The result, if the payload is observed in Stariwell, is a file object describing the file the input hash is related to."
|
||||
}
|
|
@ -8,7 +8,7 @@
|
|||
"output": "MISP attributes and objects resulting from the parsing of the VirusTotal report concerning the input attribute.",
|
||||
"references": [
|
||||
"https://www.virustotal.com/",
|
||||
"https://developers.virustotal.com/reference"
|
||||
"https://docs.virustotal.com/reference/overview"
|
||||
],
|
||||
"features": "New format of modules able to return attributes and objects.\n\nA module to take a MISP attribute as input and query the VirusTotal API to get additional data about it.\n\nCompared to the [standard VirusTotal expansion module](https://github.com/MISP/misp-modules/blob/main/misp_modules/modules/expansion/virustotal_public.py), this module is made for advanced parsing of VirusTotal report, with a recursive analysis of the elements found after the first request.\n\nThus, it requires a higher request rate limit to avoid the API to return a 204 error (Request rate limit exceeded), and the data parsed from the different requests are returned as MISP attributes and objects, with the corresponding relations between each one of them."
|
||||
}
|
|
@ -8,7 +8,7 @@
|
|||
"output": "MISP attributes and objects resulting from the parsing of the VirusTotal report concerning the input attribute.",
|
||||
"references": [
|
||||
"https://www.virustotal.com",
|
||||
"https://developers.virustotal.com/reference"
|
||||
"https://docs.virustotal.com/reference/overview"
|
||||
],
|
||||
"features": "New format of modules able to return attributes and objects.\n\nA module to take a MISP attribute as input and query the VirusTotal API to get additional data about it.\n\nCompared to the [more advanced VirusTotal expansion module](https://github.com/MISP/misp-modules/blob/main/misp_modules/modules/expansion/virustotal.py), this module is made for VirusTotal users who have a low request rate limit.\n\nThus, it only queries the API once and returns the results that is parsed into MISP attributes and objects."
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
{
|
||||
"description": "Module to enrich the information by making use of the Vysion API.",
|
||||
"logo": "vysion.png",
|
||||
"requirements": [
|
||||
"Vysion python library",
|
||||
"Vysion API Key"
|
||||
],
|
||||
"input": "MISP Attribute which include: company(target-org), country, info.",
|
||||
"output": "MISP objects containing title, link to our webapp and TOR, i2p or clearnet URLs.",
|
||||
"references": [
|
||||
"https://vysion.ai/",
|
||||
"https://developers.vysion.ai/",
|
||||
"https://github.com/ByronLabs/vysion-cti/tree/main"
|
||||
],
|
||||
"features": "This module gets correlated information from our dark web intelligence database. With this you will get several objects containing information related to, for example, an organization victim of a ransomware attack."
|
||||
}
|
|
@ -294,7 +294,8 @@ def main():
|
|||
|
||||
application = tornado.web.Application(service)
|
||||
try:
|
||||
application.listen(args.port, address=args.listen)
|
||||
server = tornado.httpserver.HTTPServer(application, max_buffer_size=1073741824) # buffer size increase when large MISP event are submitted - GH issue 662
|
||||
server.listen(args.port, args.listen)
|
||||
except Exception as e:
|
||||
if e.errno == 98:
|
||||
pids = psutil.pids()
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit 9c8b9504257c65459cfedf4f3231aee74f77dbe3
|
||||
Subproject commit a193e03ad200baddcdc0d5fad1cc1d8bd1276b7f
|
|
@ -4,10 +4,10 @@ This module provides rules that helps MISP importers to connect MISP attributes
|
|||
between them using VirusTotal relationship. Check all available relationship
|
||||
here:
|
||||
|
||||
- File: https://developers.virustotal.com/v3/reference/#files-relationships
|
||||
- URL: https://developers.virustotal.com/v3/reference/#urls-relationships
|
||||
- Domain: https://developers.virustotal.com/v3/reference/#domains-relationships
|
||||
- IP: https://developers.virustotal.com/v3/reference/#ip-relationships
|
||||
- File: https://docs.virustotal.com/reference/files#relationships
|
||||
- URL: https://docs.virustotal.com/reference/url-object#relationships
|
||||
- Domain: https://docs.virustotal.com/reference/domains-object#relationships
|
||||
- IP: https://docs.virustotal.com/reference/ip-object#relationships
|
||||
"""
|
||||
|
||||
|
||||
|
|
|
@ -1 +1 @@
|
|||
__all__ = ['testaction', 'mattermost']
|
||||
__all__ = ['testaction', 'mattermost', 'slack']
|
||||
|
|
|
@ -25,6 +25,7 @@ moduleconfig = {
|
|||
'type': 'large_string',
|
||||
'description': 'The template to be used to generate the message to be posted',
|
||||
'value': 'The **template** will be rendered using *Jinja2*!',
|
||||
'jinja_supported': True,
|
||||
},
|
||||
},
|
||||
# Blocking modules break the exection of the current of action
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
import json
|
||||
from slack_sdk import WebClient
|
||||
from slack_sdk.errors import SlackApiError
|
||||
from ._utils import utils
|
||||
|
||||
misperrors = {'error': 'Error'}
|
||||
|
||||
# config fields that your code expects from the site admin
|
||||
moduleconfig = {
|
||||
'params': {
|
||||
'slack_bot_token': {
|
||||
'type': 'string',
|
||||
'description': 'The Slack bot token generated when you created the bot account',
|
||||
},
|
||||
'channel_id': {
|
||||
'type': 'string',
|
||||
'description': 'The channel ID you want to post messages to',
|
||||
},
|
||||
'message_template': {
|
||||
'type': 'large_string',
|
||||
'description': 'The template to be used to generate the message to be posted',
|
||||
'value': 'The **template** will be rendered using *Jinja2*!',
|
||||
'jinja_supported': True,
|
||||
},
|
||||
},
|
||||
# Blocking modules break the execution of the current action
|
||||
'blocking': False,
|
||||
# Indicates whether parts of the data passed to this module should be filtered.
|
||||
'support_filters': True,
|
||||
# Indicates whether the data passed to this module should be compliant with the MISP core format
|
||||
'expect_misp_core_format': False,
|
||||
}
|
||||
|
||||
# returns either "boolean" or "data"
|
||||
# Boolean is used to simply signal that the execution has finished.
|
||||
# For blocking modules, the actual boolean value determines whether we break execution
|
||||
returns = 'boolean'
|
||||
|
||||
moduleinfo = {'version': '0.1', 'author': 'goodlandsecurity',
|
||||
'description': 'Simplistic module to send messages to a Slack channel.',
|
||||
'module-type': ['action']}
|
||||
|
||||
|
||||
def create_post(request):
|
||||
params = request['params']
|
||||
slack_token = params['slack_bot_token']
|
||||
channel_id = params['channel_id']
|
||||
|
||||
client = WebClient(token=slack_token)
|
||||
|
||||
data = request.get('matchingData', request.get('data', {}))
|
||||
|
||||
if params['message_template']:
|
||||
message = utils.renderTemplate(data, params['message_template'])
|
||||
else:
|
||||
message = '```\n{}\n```'.format(json.dumps(data))
|
||||
|
||||
try:
|
||||
client.chat_postMessage(channel=channel_id, text=message)
|
||||
return True
|
||||
except SlackApiError as e:
|
||||
error_message = e.response['error']
|
||||
print(f"Error posting message: {error_message}")
|
||||
return False
|
||||
|
||||
|
||||
def handler(q=False):
|
||||
if q is False:
|
||||
return False
|
||||
request = json.loads(q)
|
||||
create_post(request)
|
||||
return {"data": True}
|
||||
|
||||
|
||||
def introspection():
|
||||
modulesetup = {}
|
||||
try:
|
||||
modulesetup['config'] = moduleconfig
|
||||
except NameError:
|
||||
pass
|
||||
return modulesetup
|
||||
|
||||
|
||||
def version():
|
||||
moduleinfo['config'] = moduleconfig
|
||||
return moduleinfo
|
|
@ -20,7 +20,8 @@ __all__ = ['cuckoo_submit', 'vmray_submit', 'bgpranking', 'circl_passivedns', 'c
|
|||
'trustar_enrich', 'recordedfuture', 'html_to_markdown', 'socialscan', 'passive-ssh',
|
||||
'qintel_qsentry', 'mwdb', 'hashlookup', 'mmdb_lookup', 'ipqs_fraud_and_risk_scoring',
|
||||
'clamav', 'jinja_template_rendering','hyasinsight', 'variotdbs', 'crowdsec',
|
||||
'extract_url_components', 'ipinfo', 'whoisfreaks', 'ip2locationio', 'vysion']
|
||||
'extract_url_components', 'ipinfo', 'whoisfreaks', 'ip2locationio', 'stairwell',
|
||||
'google_threat_intelligence', 'vulnerability_lookup']
|
||||
|
||||
|
||||
minimum_required_fields = ('type', 'uuid', 'value')
|
||||
|
|
|
@ -0,0 +1,118 @@
|
|||
import json
|
||||
from pymisp import MISPAttribute, MISPEvent, MISPObject
|
||||
|
||||
|
||||
class VulnerabilityMapping:
|
||||
__variot_data_mapping = {
|
||||
'credits': 'credit',
|
||||
'description': 'description',
|
||||
'title': 'summary'
|
||||
}
|
||||
__variot_flat_mapping = {
|
||||
'cve': 'id', 'id': 'id'
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def exploit_mapping(cls) -> dict:
|
||||
return cls.__exploit_mapping
|
||||
|
||||
@classmethod
|
||||
def exploit_multiple_mapping(cls) -> dict:
|
||||
return cls.__exploit_multiple_mapping
|
||||
|
||||
@classmethod
|
||||
def variot_data_mapping(cls) -> dict:
|
||||
return cls.__variot_data_mapping
|
||||
|
||||
@classmethod
|
||||
def variot_flat_mapping(cls) -> dict:
|
||||
return cls.__variot_flat_mapping
|
||||
|
||||
|
||||
class VulnerabilityParser:
|
||||
def __init__(self, attribute: dict):
|
||||
misp_attribute = MISPAttribute()
|
||||
misp_attribute.from_dict(**attribute)
|
||||
misp_event = MISPEvent()
|
||||
misp_event.add_attribute(**misp_attribute)
|
||||
self.__misp_attribute = misp_attribute
|
||||
self.__misp_event = misp_event
|
||||
|
||||
@property
|
||||
def misp_attribute(self):
|
||||
return self.__misp_attribute
|
||||
|
||||
@property
|
||||
def misp_event(self):
|
||||
return self.__misp_event
|
||||
|
||||
def get_results(self) -> dict:
|
||||
event = json.loads(self.misp_event.to_json())
|
||||
return {
|
||||
'results': {
|
||||
key: value for key, value in event.items()
|
||||
if key in ('Attribute', 'Object')
|
||||
}
|
||||
}
|
||||
|
||||
def _parse_variot_description(self, query_results):
|
||||
vulnerability_object = MISPObject('vulnerability')
|
||||
for field, relation in self.mapping.variot_flat_mapping().items():
|
||||
if query_results.get(field):
|
||||
vulnerability_object.add_attribute(
|
||||
relation, query_results[field]
|
||||
)
|
||||
for field, relation in self.mapping.variot_data_mapping().items():
|
||||
if query_results.get(field, {}).get('data'):
|
||||
vulnerability_object.add_attribute(
|
||||
relation, query_results[field]['data']
|
||||
)
|
||||
if query_results.get('configurations', {}).get('data'):
|
||||
for configuration in query_results['configurations']['data']:
|
||||
for node in configuration['nodes']:
|
||||
for cpe_match in node['cpe_match']:
|
||||
if cpe_match['vulnerable']:
|
||||
vulnerability_object.add_attribute(
|
||||
'vulnerable-configuration',
|
||||
cpe_match['cpe23Uri']
|
||||
)
|
||||
if query_results.get('cvss', {}).get('data'):
|
||||
cvss = {}
|
||||
for cvss_data in query_results['cvss']['data']:
|
||||
for cvss_v3 in cvss_data['cvssV3']:
|
||||
cvss[float(cvss_v3['trust'])] = cvss_v3
|
||||
if cvss:
|
||||
cvss = cvss[max(cvss)]
|
||||
vulnerability_object.add_attribute(
|
||||
'cvss-score', cvss['baseScore']
|
||||
)
|
||||
vulnerability_object.add_attribute(
|
||||
'cvss-string', cvss['vectorString']
|
||||
)
|
||||
if query_results.get('references', {}).get('data'):
|
||||
for reference in query_results['references']['data']:
|
||||
vulnerability_object.add_attribute(
|
||||
'references', reference['url']
|
||||
)
|
||||
if query_results.get('sources_release_date', {}).get('data'):
|
||||
for release_date in query_results['sources_release_date']['data']:
|
||||
if release_date['db'] != 'NVD':
|
||||
continue
|
||||
if release_date['id'] == self.misp_attribute.value:
|
||||
vulnerability_object.add_attribute(
|
||||
'published', release_date['date']
|
||||
)
|
||||
break
|
||||
if query_results.get('sources_update_date', {}).get('data'):
|
||||
for update_date in query_results['sources_update_date']['data']:
|
||||
if update_date['db'] != 'NVD':
|
||||
continue
|
||||
if update_date['id'] == self.misp_attribute.value:
|
||||
vulnerability_object.add_attribute(
|
||||
'modified', update_date['date']
|
||||
)
|
||||
break
|
||||
vulnerability_object.add_reference(
|
||||
self.misp_attribute.uuid, 'related-to'
|
||||
)
|
||||
self.misp_event.add_object(vulnerability_object)
|
|
@ -4,7 +4,7 @@ import dns.resolver
|
|||
misperrors = {'error': 'Error'}
|
||||
mispattributes = {'input': ['hostname', 'domain', 'domain|ip'], 'output': ['ip-src',
|
||||
'ip-dst']}
|
||||
moduleinfo = {'version': '0.2', 'author': 'Alexandre Dulaunoy',
|
||||
moduleinfo = {'version': '0.3', 'author': 'Alexandre Dulaunoy',
|
||||
'description': 'Simple DNS expansion service to resolve IP address from MISP attributes',
|
||||
'module-type': ['expansion', 'hover']}
|
||||
|
||||
|
@ -43,8 +43,8 @@ def handler(q=False):
|
|||
except dns.exception.Timeout:
|
||||
misperrors['error'] = "Timeout"
|
||||
return misperrors
|
||||
except Exception:
|
||||
misperrors['error'] = "DNS resolving error"
|
||||
except Exception as e:
|
||||
misperrors['error'] = f'DNS resolving error {e}'
|
||||
return misperrors
|
||||
|
||||
r = {'results': [{'types': mispattributes['output'],
|
||||
|
|
|
@ -0,0 +1,322 @@
|
|||
#!/usr/local/bin/python
|
||||
# Copyright © 2024 The Google Threat Intelligence authors. All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Google Threat Intelligence MISP expansion module."""
|
||||
|
||||
from urllib import parse
|
||||
import vt
|
||||
import pymisp
|
||||
|
||||
|
||||
MISP_ATTRIBUTES = {
|
||||
'input': [
|
||||
'hostname',
|
||||
'domain',
|
||||
'ip-src',
|
||||
'ip-dst',
|
||||
'md5',
|
||||
'sha1',
|
||||
'sha256',
|
||||
'url',
|
||||
],
|
||||
'format': 'misp_standard',
|
||||
}
|
||||
|
||||
MODULE_INFO = {
|
||||
'version': '1',
|
||||
'author': 'Google Threat Intelligence team',
|
||||
'description': ('An expansion module to have the observable\'s threat'
|
||||
' score assessed by Google Threat Intelligence.'),
|
||||
'module-type': ['expansion'],
|
||||
'config': [
|
||||
'apikey',
|
||||
'event_limit',
|
||||
'proxy_host',
|
||||
'proxy_port',
|
||||
'proxy_username',
|
||||
'proxy_password'
|
||||
]
|
||||
}
|
||||
|
||||
DEFAULT_RESULTS_LIMIT = 10
|
||||
|
||||
|
||||
class GoogleThreatIntelligenceParser:
|
||||
"""Main parser class to create the MISP event."""
|
||||
def __init__(self, client: vt.Client, limit: int) -> None:
|
||||
self.client = client
|
||||
self.limit = limit or DEFAULT_RESULTS_LIMIT
|
||||
self.misp_event = pymisp.MISPEvent()
|
||||
self.attribute = pymisp.MISPAttribute()
|
||||
self.parsed_objects = {}
|
||||
self.input_types_mapping = {
|
||||
'ip-src': self.parse_ip,
|
||||
'ip-dst': self.parse_ip,
|
||||
'domain': self.parse_domain,
|
||||
'hostname': self.parse_domain,
|
||||
'md5': self.parse_hash,
|
||||
'sha1': self.parse_hash,
|
||||
'sha256': self.parse_hash,
|
||||
'url': self.parse_url
|
||||
}
|
||||
self.proxies = None
|
||||
|
||||
def query_api(self, attribute: dict) -> None:
|
||||
"""Get data from the API and parse it."""
|
||||
self.attribute.from_dict(**attribute)
|
||||
self.input_types_mapping[self.attribute.type](self.attribute.value)
|
||||
|
||||
def get_results(self) -> dict:
|
||||
"""Serialize the MISP event."""
|
||||
event = self.misp_event.to_dict()
|
||||
results = {
|
||||
key: event[key] for key in ('Attribute', 'Object') \
|
||||
if (key in event and event[key])
|
||||
}
|
||||
return {'results': results}
|
||||
|
||||
def create_gti_report_object(self, report):
|
||||
"""Create GTI report object."""
|
||||
report = report.to_dict()
|
||||
permalink = ('https://www.virustotal.com/gui/'
|
||||
f"{report['type']}/{report['id']}")
|
||||
report_object = pymisp.MISPObject('Google-Threat-Intel-report')
|
||||
report_object.add_attribute('permalink', type='link', value=permalink)
|
||||
report_object.add_attribute(
|
||||
'Threat Score', type='text',
|
||||
value=get_key(
|
||||
report, 'attributes.gti_assessment.threat_score.value'))
|
||||
report_object.add_attribute(
|
||||
'Verdict', type='text',
|
||||
value=get_key(
|
||||
report, 'attributes.gti_assessment.verdict.value').replace(
|
||||
'VERDICT_', ''))
|
||||
report_object.add_attribute(
|
||||
'Severity', type='text',
|
||||
value=get_key(
|
||||
report, 'attributes.gti_assessment.severity.value').replace(
|
||||
'SEVERITY_', ''))
|
||||
report_object.add_attribute(
|
||||
'Threat Label', type='text',
|
||||
value=get_key(
|
||||
report, ('attributes.popular_threat_classification'
|
||||
'.suggested_threat_label')))
|
||||
self.misp_event.add_object(**report_object)
|
||||
return report_object.uuid
|
||||
|
||||
def parse_domain(self, domain: str) -> str:
|
||||
"""Create domain MISP object."""
|
||||
domain_report = self.client.get_object(f'/domains/{domain}')
|
||||
|
||||
# DOMAIN
|
||||
domain_object = pymisp.MISPObject('domain-ip')
|
||||
domain_object.add_attribute(
|
||||
'domain', type='domain', value=domain_report.id)
|
||||
|
||||
report_uuid = self.create_gti_report_object(domain_report)
|
||||
domain_object.add_reference(report_uuid, 'analyzed-with')
|
||||
self.misp_event.add_object(**domain_object)
|
||||
return domain_object.uuid
|
||||
|
||||
def parse_hash(self, file_hash: str) -> str:
|
||||
"""Create hash MISP object."""
|
||||
file_report = self.client.get_object(f'/files/{file_hash}')
|
||||
file_object = pymisp.MISPObject('file')
|
||||
for hash_type in ('md5', 'sha1', 'sha256'):
|
||||
file_object.add_attribute(
|
||||
hash_type,
|
||||
**{'type': hash_type, 'value': file_report.get(hash_type)})
|
||||
|
||||
report_uuid = self.create_gti_report_object(file_report)
|
||||
file_object.add_reference(report_uuid, 'analyzed-with')
|
||||
self.misp_event.add_object(**file_object)
|
||||
return file_object.uuid
|
||||
|
||||
def parse_ip(self, ip: str) -> str:
|
||||
"""Create ip MISP object."""
|
||||
ip_report = self.client.get_object(f'/ip_addresses/{ip}')
|
||||
|
||||
# IP
|
||||
ip_object = pymisp.MISPObject('domain-ip')
|
||||
ip_object.add_attribute('ip', type='ip-dst', value=ip_report.id)
|
||||
|
||||
report_uuid = self.create_gti_report_object(ip_report)
|
||||
ip_object.add_reference(report_uuid, 'analyzed-with')
|
||||
self.misp_event.add_object(**ip_object)
|
||||
return ip_object.uuid
|
||||
|
||||
def parse_url(self, url: str) -> str:
|
||||
"""Create URL MISP object."""
|
||||
url_id = vt.url_id(url)
|
||||
url_report = self.client.get_object(f'/urls/{url_id}')
|
||||
|
||||
url_object = pymisp.MISPObject('url')
|
||||
url_object.add_attribute('url', type='url', value=url_report.url)
|
||||
|
||||
report_uuid = self.create_gti_report_object(url_report)
|
||||
url_object.add_reference(report_uuid, 'analyzed-with')
|
||||
self.misp_event.add_object(**url_object)
|
||||
return url_object.uuid
|
||||
|
||||
|
||||
def get_key(dictionary, key, default_value=''):
|
||||
"""Get value from nested dictionaries."""
|
||||
dictionary = dictionary or {}
|
||||
keys = key.split('.')
|
||||
field_name = keys.pop()
|
||||
for k in keys:
|
||||
if k not in dictionary:
|
||||
return default_value
|
||||
dictionary = dictionary[k]
|
||||
return dictionary.get(field_name, default_value)
|
||||
|
||||
|
||||
def get_proxy_settings(config: dict) -> dict:
|
||||
"""Returns proxy settings in the requests format or None if not set up."""
|
||||
proxies = None
|
||||
host = config.get('proxy_host')
|
||||
port = config.get('proxy_port')
|
||||
username = config.get('proxy_username')
|
||||
password = config.get('proxy_password')
|
||||
|
||||
if host:
|
||||
if not port:
|
||||
raise KeyError(
|
||||
('The google_threat_intelligence_proxy_host config is set, '
|
||||
'please also set the virustotal_proxy_port.'))
|
||||
parsed = parse.urlparse(host)
|
||||
if 'http' in parsed.scheme:
|
||||
scheme = 'http'
|
||||
else:
|
||||
scheme = parsed.scheme
|
||||
netloc = parsed.netloc
|
||||
host = f'{netloc}:{port}'
|
||||
|
||||
if username:
|
||||
if not password:
|
||||
raise KeyError(('The google_threat_intelligence_'
|
||||
' proxy_host config is set, please also'
|
||||
' set the virustotal_proxy_password.'))
|
||||
auth = f'{username}:{password}'
|
||||
host = auth + '@' + host
|
||||
|
||||
proxies = {
|
||||
'http': f'{scheme}://{host}',
|
||||
'https': f'{scheme}://{host}'
|
||||
}
|
||||
return proxies
|
||||
|
||||
|
||||
def dict_handler(request: dict):
|
||||
"""MISP entry point fo the module."""
|
||||
if not request.get('config') or not request['config'].get('apikey'):
|
||||
return {
|
||||
'error': ('A Google Threat Intelligence api '
|
||||
'key is required for this module.')
|
||||
}
|
||||
|
||||
if not request.get('attribute'):
|
||||
return {
|
||||
'error': ('This module requires an "attribute" field as input,'
|
||||
' which should contain at least a type, a value and an'
|
||||
' uuid.')
|
||||
}
|
||||
|
||||
if request['attribute']['type'] not in MISP_ATTRIBUTES['input']:
|
||||
return {'error': 'Unsupported attribute type.'}
|
||||
|
||||
event_limit = request['config'].get('event_limit')
|
||||
attribute = request['attribute']
|
||||
|
||||
try:
|
||||
proxy_settings = get_proxy_settings(request.get('config'))
|
||||
client = vt.Client(
|
||||
request['config']['apikey'],
|
||||
headers={
|
||||
'x-tool': 'MISPModuleGTIExpansion',
|
||||
},
|
||||
proxy=proxy_settings['http'] if proxy_settings else None)
|
||||
parser = GoogleThreatIntelligenceParser(
|
||||
client, int(event_limit) if event_limit else None)
|
||||
parser.query_api(attribute)
|
||||
except vt.APIError as ex:
|
||||
return {'error': ex.message}
|
||||
except KeyError as ex:
|
||||
return {'error': str(ex)}
|
||||
|
||||
return parser.get_results()
|
||||
|
||||
|
||||
def introspection():
|
||||
"""Returns the module input attributes required."""
|
||||
return MISP_ATTRIBUTES
|
||||
|
||||
|
||||
def version():
|
||||
"""Returns the module metadata."""
|
||||
return MODULE_INFO
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Testing/debug calls.
|
||||
import os
|
||||
api_key = os.getenv('GTI_API_KEY')
|
||||
# File
|
||||
request_data = {
|
||||
'config': {'apikey': api_key},
|
||||
'attribute': {
|
||||
'type': 'sha256',
|
||||
'value': ('ed01ebfbc9eb5bbea545af4d01bf5f10'
|
||||
'71661840480439c6e5babe8e080e41aa')
|
||||
}
|
||||
}
|
||||
response = dict_handler(request_data)
|
||||
report_obj = response['results']['Object'][0]
|
||||
print(report_obj.to_dict())
|
||||
|
||||
# URL
|
||||
request_data = {
|
||||
'config': {'apikey': api_key},
|
||||
'attribute': {
|
||||
'type': 'url',
|
||||
'value': 'http://47.21.48.182:60813/Mozi.a'
|
||||
}
|
||||
}
|
||||
response = dict_handler(request_data)
|
||||
report_obj = response['results']['Object'][0]
|
||||
print(report_obj.to_dict())
|
||||
|
||||
# Ip
|
||||
request_data = {
|
||||
'config': {'apikey': api_key},
|
||||
'attribute': {
|
||||
'type': 'ip-src',
|
||||
'value': '180.72.148.38'
|
||||
}
|
||||
}
|
||||
response = dict_handler(request_data)
|
||||
report_obj = response['results']['Object'][0]
|
||||
print(report_obj.to_dict())
|
||||
|
||||
# Domain
|
||||
request_data = {
|
||||
'config': {'apikey': api_key},
|
||||
'attribute': {
|
||||
'type': 'domain',
|
||||
'value': 'qexyhuv.com'
|
||||
}
|
||||
}
|
||||
response = dict_handler(request_data)
|
||||
report_obj = response['results']['Object'][0]
|
||||
print(report_obj.to_dict())
|
|
@ -6,10 +6,8 @@ from pyipasnhistory import IPASNHistory
|
|||
from pymisp import MISPAttribute, MISPEvent, MISPObject
|
||||
|
||||
misperrors = {'error': 'Error'}
|
||||
mispattributes = {'input': ['ip-src', 'ip-dst'], 'format': 'misp_standard'}
|
||||
moduleinfo = {'version': '0.2', 'author': 'Raphaël Vinot',
|
||||
'description': 'Query an IP ASN history service (https://github.com/CIRCL/IP-ASN-history.git)',
|
||||
'module-type': ['expansion', 'hover']}
|
||||
mispattributes = {'input': ['ip-src', 'ip-dst', 'ip'], 'format': 'misp_standard'}
|
||||
moduleinfo = {'version': '0.3', 'author': 'Raphaël Vinot', 'description': 'Query an IP ASN history service (https://github.com/D4-project/IPASN-History?tab=readme-ov-file)', 'module-type': ['expansion', 'hover']}
|
||||
|
||||
|
||||
def parse_result(attribute, values):
|
||||
|
@ -18,7 +16,6 @@ def parse_result(attribute, values):
|
|||
initial_attribute.from_dict(**attribute)
|
||||
event.add_attribute(**initial_attribute)
|
||||
mapping = {'asn': ('AS', 'asn'), 'prefix': ('ip-src', 'subnet-announced')}
|
||||
print(values)
|
||||
for last_seen, response in values['response'].items():
|
||||
asn = MISPObject('asn')
|
||||
asn.add_attribute('last-seen', **{'type': 'datetime', 'value': last_seen})
|
||||
|
@ -39,6 +36,9 @@ def handler(q=False):
|
|||
return {'error': f'{standard_error_message}, which should contain at least a type, a value and an uuid.'}
|
||||
if request['attribute']['type'] not in mispattributes['input']:
|
||||
return {'error': 'Unsupported attribute type.'}
|
||||
if request['attribute']['type'] == 'ip':
|
||||
request['attribute']['type'] = 'ip-src'
|
||||
|
||||
toquery = request['attribute']['value']
|
||||
|
||||
ipasn = IPASNHistory()
|
||||
|
|
|
@ -36,7 +36,9 @@ def handler(q=False):
|
|||
num_sheets = len(doc.sheets)
|
||||
try:
|
||||
for i in range(0, num_sheets):
|
||||
ods = pandas_ods_reader.algo.read_data(pandas_ods_reader.parsers.ods, ods_file, i, headers=False)
|
||||
rows = pandas_ods_reader.parsers.ods.get_rows(doc, i)
|
||||
ods = pandas_ods_reader.algo.parse_data(pandas_ods_reader.parsers.ods, rows, headers=False, columns=[], skiprows=0)
|
||||
ods = pandas_ods_reader.utils.sanitize_df(ods)
|
||||
ods_content = ods_content + "\n" + ods.to_string(max_rows=None)
|
||||
return {'results': [{'types': ['freetext'], 'values': ods_content, 'comment': ".ods-to-text from file " + filename},
|
||||
{'types': ['text'], 'values': ods_content, 'comment': ".ods-to-text from file " + filename}]}
|
||||
|
|
|
@ -0,0 +1,149 @@
|
|||
import json
|
||||
import re
|
||||
import requests
|
||||
from pymisp import MISPEvent, MISPObject
|
||||
from . import check_input_attribute, checking_error, standard_error_message
|
||||
|
||||
|
||||
misperrors = {
|
||||
'error': 'Error'
|
||||
}
|
||||
mispattributes = {
|
||||
'input': [
|
||||
'md5',
|
||||
'sha1',
|
||||
'sha256'
|
||||
],
|
||||
'format': 'misp_standard'
|
||||
}
|
||||
moduleinfo = {
|
||||
'version': '0.1',
|
||||
'author': 'goodlandsecurity',
|
||||
'description': 'Enrich hash observables with the Stairwell API',
|
||||
'module-type': ['expansion']
|
||||
}
|
||||
moduleconfig = ["apikey"]
|
||||
|
||||
|
||||
def parse_response(response: dict):
|
||||
attribute_mapping = {
|
||||
'environments': {'type': 'comment', 'object_relation': 'environment', 'distribution': 5},
|
||||
'imphash': {'type': 'imphash', 'object_relation': 'impash', 'distribution': 5},
|
||||
'magic': {'type': 'comment', 'object_relation': 'magic', 'distribution': 5},
|
||||
'malEval': {
|
||||
'probabilityBucket': {'type': 'comment', 'object_relation': 'malEval-probability', 'distribution': 5},
|
||||
'severity': {'type': 'comment', 'object_relation': 'malEval-severity', 'distribution': 5}
|
||||
},
|
||||
'md5': {'type': 'md5', 'object_relation': 'md5', 'distribution': 5},
|
||||
'mimeType': {'type': 'mime-type', 'object_relation': 'mime-type', 'distribution': 5},
|
||||
'sha1': {'type': 'sha1', 'object_relation': 'sha1', 'distribution': 5},
|
||||
'sha256': {'type': 'sha256', 'object_relation': 'sha256', 'distribution': 5},
|
||||
'shannonEntropy': {'type': 'float', 'object_relation': 'entropy', 'distribution': 5},
|
||||
'size': {'type': 'size-in-bytes', 'object_relation': 'size-in-bytes', 'distribution': 5},
|
||||
'stairwellFirstSeenTime': {'type': 'datetime', 'object_relation': 'stairwell-first-seen', 'distribution': 5},
|
||||
'tlsh': {'type': 'tlsh', 'object_relation': 'tlsh', 'distribution': 5},
|
||||
'yaraRuleMatches': {'type': 'text', 'object_relation': 'yara-rule-match', 'comment': 'matching Stairwell yara rule name', 'distribution': 5}
|
||||
}
|
||||
environments_mapping = {
|
||||
"NCS2SM-YHB2KT-SAFUDX-JC7F6WYA": "Florian's Open Rules",
|
||||
"VR9Z98-4KU7ZC-PCNFEG-FURQ66FW": "Jotti",
|
||||
"D7W6M6-BA9BS4-BQ23Z4-NKCNWQ96": "Malshare",
|
||||
"D4447Q-WJJL6P-W7ME89-WHXJK8TW": "Malware Bazaar",
|
||||
"XAKLND-DKWP3Z-56RL88-6XJ5NH46": "Pro Rules",
|
||||
"GMEELM-K226XF-F95XZL-7VEJFKZ6": "Public Samples",
|
||||
"5HEG8N-9T7UPG-8SZJ7T-2J4XSDC6": "RH-ISAC",
|
||||
"2NN2BJ-HDVQHS-49824H-2SEDBBLJ": "RH-ISAC Malware Sharing",
|
||||
"VCZTNF-8S76AK-LUU53W-2SWFFZWJ": "Stairwell Experimental Rules",
|
||||
"GEG6FU-MRARGF-TLTM6X-H6MGDT5E": "Stairwell Methodology Rules",
|
||||
"EB3DXY-3ZYFVH-6HNKJQ-GAPKHESS": "Stairwell OSINT Rules",
|
||||
"NQNJM6-5LSCAF-3MC5FJ-W8EKGW6N": "Stairwell Research Rules",
|
||||
"TT9GM5-JUMD8H-9828FL-GAW5NNXE": "stairwell-public-verdicts",
|
||||
"MKYSAR-3XN9MB-3VAK3R-888ZJUTJ": "Threat Report Feeds",
|
||||
"6HP5R3-ZM7DAN-RB4732-X6QPCJ36": "Virusshare",
|
||||
"TV6WCV-7Y79LE-BK79EY-C8GUEY46": "vxintel"
|
||||
}
|
||||
|
||||
misp_event = MISPEvent()
|
||||
misp_object = MISPObject('stairwell')
|
||||
for feature, attribute in attribute_mapping.items():
|
||||
if feature in response.keys() and response[feature]:
|
||||
if feature == 'yaraRuleMatches':
|
||||
for rule in response[feature]:
|
||||
env_pattern = r'\b[A-Z0-9]{6}-[A-Z0-9]{6}-[A-Z0-9]{6}-[A-Z0-9]{8}\b'
|
||||
env = re.findall(env_pattern, rule.split('yaraRules/')[0])[0]
|
||||
misp_attribute = {
|
||||
'value': rule.split('yaraRules/')[1],
|
||||
'comment': f'Rule from: {environments_mapping.get(env, "Unknown UUID!")}'
|
||||
}
|
||||
misp_attribute.update(attribute)
|
||||
misp_object.add_attribute(**misp_attribute)
|
||||
elif feature == 'environments':
|
||||
for env in response[feature]:
|
||||
misp_attribute = {
|
||||
'value': environments_mapping.get(env, f'Unknown Environment: {env}'),
|
||||
'comment': 'Hash observed in'
|
||||
}
|
||||
misp_attribute.update(attribute)
|
||||
misp_object.add_attribute(**misp_attribute)
|
||||
elif feature == 'malEval':
|
||||
for attr in attribute:
|
||||
misp_attribute = {'value': response[feature][attr]}
|
||||
misp_attribute.update(attribute[attr])
|
||||
misp_object.add_attribute(**misp_attribute)
|
||||
else:
|
||||
misp_attribute = {'value': response[feature]}
|
||||
misp_attribute.update(attribute)
|
||||
attr = misp_object.add_attribute(**misp_attribute)
|
||||
if feature in ('md5', 'sha1', 'sha256'):
|
||||
for label in response['malEval']['labels']:
|
||||
attr.add_tag(label)
|
||||
misp_event.add_object(**misp_object)
|
||||
|
||||
event = json.loads(misp_event.to_json())
|
||||
results = {'Object': event['Object']}
|
||||
|
||||
return {'results': results}
|
||||
|
||||
|
||||
def handler(q=False):
|
||||
if q is False:
|
||||
return False
|
||||
request = json.loads(q)
|
||||
if not request.get('config') or not request['config'].get('apikey'):
|
||||
misperrors['error'] = 'A Stairwell api key is required for this module!'
|
||||
return misperrors
|
||||
if not request.get('attribute') or not check_input_attribute(request['attribute'], requirements=('type', 'value')):
|
||||
misperrors['error'] = f'{standard_error_message}, {checking_error}.'
|
||||
return misperrors
|
||||
attribute = request['attribute']
|
||||
if attribute['type'] not in mispattributes['input']:
|
||||
misperrors['error'] = 'Unsupported attribute type!'
|
||||
return misperrors
|
||||
|
||||
headers = {
|
||||
"Accept": "application/json",
|
||||
"Authorization": request['config']['apikey'],
|
||||
"User-Agent": f"misp-module {__file__} {moduleinfo['version']}"
|
||||
}
|
||||
url = f"https://app.stairwell.com/v1/objects/{attribute['value']}/metadata"
|
||||
response = requests.get(url=url, headers=headers).json()
|
||||
|
||||
if response.get('code') == 16: # bad auth
|
||||
return {'error': f"{response['message']} Is api key valid?"}
|
||||
elif response.get('code') == 5: # not found
|
||||
return {'error': f"{attribute['type']}:{attribute['value']} {response['message']}"}
|
||||
elif response.get('code') == 2: # encoding/hex: invalid byte
|
||||
return {'error': response['message']}
|
||||
elif response.get('code'): # catchall for potential unforeseen errors
|
||||
return {'error': response['message'], 'code': response['code']}
|
||||
else:
|
||||
return parse_response(response)
|
||||
|
||||
|
||||
def introspection():
|
||||
return mispattributes
|
||||
|
||||
|
||||
def version():
|
||||
moduleinfo['config'] = moduleconfig
|
||||
return moduleinfo
|
|
@ -1,7 +1,9 @@
|
|||
import json
|
||||
import requests
|
||||
from . import check_input_attribute, standard_error_message
|
||||
from pymisp import MISPAttribute, MISPEvent, MISPObject
|
||||
from ._vulnerability_parser.vulnerability_parser import (
|
||||
VulnerabilityMapping, VulnerabilityParser)
|
||||
from pymisp import MISPObject
|
||||
|
||||
misperrors = {'error': 'Error'}
|
||||
mispattributes = {'input': ['vulnerability'], 'format': 'misp_standard'}
|
||||
|
@ -12,155 +14,60 @@ moduleconfig = ['API_key']
|
|||
variotdbs_url = 'https://www.variotdbs.pl/api'
|
||||
|
||||
|
||||
class VariotdbsParser:
|
||||
class VariotMapping(VulnerabilityMapping):
|
||||
__exploit_mapping = {
|
||||
'credits': 'credit',
|
||||
'description': 'description',
|
||||
'exploit': 'exploit',
|
||||
'title': 'title'
|
||||
}
|
||||
__exploit_multiple_mapping = {
|
||||
'cve': {
|
||||
'feature': 'cve_id',
|
||||
'relation': 'cve-id'
|
||||
},
|
||||
'references': {
|
||||
'feature': 'url',
|
||||
'relation': 'reference'
|
||||
}
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def exploit_mapping(cls) -> dict:
|
||||
return cls.__exploit_mapping
|
||||
|
||||
@classmethod
|
||||
def exploit_multiple_mapping(cls) -> dict:
|
||||
return cls.__exploit_multiple_mapping
|
||||
|
||||
|
||||
class VariotdbsParser(VulnerabilityParser):
|
||||
def __init__(self, attribute):
|
||||
misp_attribute = MISPAttribute()
|
||||
misp_attribute.from_dict(**attribute)
|
||||
misp_event = MISPEvent()
|
||||
misp_event.add_attribute(**misp_attribute)
|
||||
self.__misp_attribute = misp_attribute
|
||||
self.__misp_event = misp_event
|
||||
self.__exploit_mapping = {
|
||||
'credits': 'credit',
|
||||
'description': 'description',
|
||||
'exploit': 'exploit',
|
||||
'title': 'title'
|
||||
}
|
||||
self.__exploit_multiple_mapping = {
|
||||
'cve': {
|
||||
'feature': 'cve_id',
|
||||
'relation': 'cve-id'
|
||||
},
|
||||
'references': {
|
||||
'feature': 'url',
|
||||
'relation': 'reference'
|
||||
}
|
||||
}
|
||||
self.__vulnerability_data_mapping = {
|
||||
'credits': 'credit',
|
||||
'description': 'description',
|
||||
'title': 'summary'
|
||||
}
|
||||
self.__vulnerability_flat_mapping = {
|
||||
'cve': 'id', 'id': 'id'
|
||||
}
|
||||
super().__init__(attribute)
|
||||
self.__mapping = VulnerabilityMapping
|
||||
|
||||
@property
|
||||
def exploit_mapping(self) -> dict:
|
||||
return self.__exploit_mapping
|
||||
|
||||
@property
|
||||
def exploit_multiple_mapping(self) -> dict:
|
||||
return self.__exploit_multiple_mapping
|
||||
|
||||
@property
|
||||
def misp_attribute(self) -> MISPAttribute:
|
||||
return self.__misp_attribute
|
||||
|
||||
@property
|
||||
def misp_event(self) -> MISPEvent:
|
||||
return self.__misp_event
|
||||
|
||||
@property
|
||||
def vulnerability_data_mapping(self) -> dict:
|
||||
return self.__vulnerability_data_mapping
|
||||
|
||||
@property
|
||||
def vulnerability_flat_mapping(self) -> dict:
|
||||
return self.__vulnerability_flat_mapping
|
||||
|
||||
def get_results(self):
|
||||
event = json.loads(self.misp_event.to_json())
|
||||
results = {key: event[key] for key in ('Attribute', 'Object') if event.get(key)}
|
||||
return {'results': results}
|
||||
def mapping(self) -> VulnerabilityMapping:
|
||||
return self.__mapping
|
||||
|
||||
def parse_exploit_information(self, query_results):
|
||||
for exploit in query_results:
|
||||
exploit_object = MISPObject('exploit')
|
||||
exploit_object.add_attribute('exploitdb-id', exploit['edb_id'])
|
||||
for feature, relation in self.exploit_mapping.items():
|
||||
if exploit.get(feature):
|
||||
for field, relation in self.mapping.exploit_mapping().items():
|
||||
if exploit.get(field):
|
||||
exploit_object.add_attribute(
|
||||
relation,
|
||||
exploit[feature]['data']
|
||||
relation, exploit[field]['data']
|
||||
)
|
||||
for feature, relation in self.exploit_multiple_mapping.items():
|
||||
if exploit.get(feature):
|
||||
for value in exploit[feature]['data']:
|
||||
for field, relation in self.mapping.exploit_multiple_mapping().items():
|
||||
if exploit.get(field):
|
||||
for value in exploit[field]['data']:
|
||||
exploit_object.add_attribute(
|
||||
relation['relation'],
|
||||
value[relation['feature']]
|
||||
relation['relation'], value[relation['feature']]
|
||||
)
|
||||
exploit_object.add_reference(self.misp_attribute.uuid, 'related-to')
|
||||
self.misp_event.add_object(exploit_object)
|
||||
|
||||
def parse_vulnerability_information(self, query_results):
|
||||
vulnerability_object = MISPObject('vulnerability')
|
||||
for feature, relation in self.vulnerability_flat_mapping.items():
|
||||
if query_results.get(feature):
|
||||
vulnerability_object.add_attribute(
|
||||
relation,
|
||||
query_results[feature]
|
||||
)
|
||||
for feature, relation in self.vulnerability_data_mapping.items():
|
||||
if query_results.get(feature, {}).get('data'):
|
||||
vulnerability_object.add_attribute(
|
||||
relation,
|
||||
query_results[feature]['data']
|
||||
)
|
||||
if query_results.get('configurations', {}).get('data'):
|
||||
for configuration in query_results['configurations']['data']:
|
||||
for node in configuration['nodes']:
|
||||
for cpe_match in node['cpe_match']:
|
||||
if cpe_match['vulnerable']:
|
||||
vulnerability_object.add_attribute(
|
||||
'vulnerable-configuration',
|
||||
cpe_match['cpe23Uri']
|
||||
)
|
||||
if query_results.get('cvss', {}).get('data'):
|
||||
cvss = {}
|
||||
for cvss_data in query_results['cvss']['data']:
|
||||
for cvss_v3 in cvss_data['cvssV3']:
|
||||
cvss[float(cvss_v3['trust'])] = cvss_v3
|
||||
if cvss:
|
||||
cvss = cvss[max(cvss)]
|
||||
vulnerability_object.add_attribute(
|
||||
'cvss-score',
|
||||
cvss['baseScore']
|
||||
)
|
||||
vulnerability_object.add_attribute(
|
||||
'cvss-string',
|
||||
cvss['vectorString']
|
||||
)
|
||||
if query_results.get('references', {}).get('data'):
|
||||
for reference in query_results['references']['data']:
|
||||
vulnerability_object.add_attribute(
|
||||
'references',
|
||||
reference['url']
|
||||
)
|
||||
if query_results.get('sources_release_date', {}).get('data'):
|
||||
for release_date in query_results['sources_release_date']['data']:
|
||||
if release_date['db'] != 'NVD':
|
||||
continue
|
||||
if release_date['id'] == self.misp_attribute.value:
|
||||
vulnerability_object.add_attribute(
|
||||
'published',
|
||||
release_date['date']
|
||||
)
|
||||
break
|
||||
if query_results.get('sources_update_date', {}).get('data'):
|
||||
for update_date in query_results['sources_update_date']['data']:
|
||||
if update_date['db'] != 'NVD':
|
||||
continue
|
||||
if update_date['id'] == self.misp_attribute.value:
|
||||
vulnerability_object.add_attribute(
|
||||
'modified',
|
||||
update_date['date']
|
||||
)
|
||||
break
|
||||
vulnerability_object.add_reference(self.misp_attribute.uuid, 'related-to')
|
||||
self.misp_event.add_object(vulnerability_object)
|
||||
|
||||
|
||||
def handler(q=False):
|
||||
if q is False:
|
||||
|
@ -180,7 +87,7 @@ def handler(q=False):
|
|||
if r.status_code == 200:
|
||||
vulnerability_results = r.json()
|
||||
if vulnerability_results:
|
||||
parser.parse_vulnerability_information(vulnerability_results)
|
||||
parser._parse_variot_description(vulnerability_results)
|
||||
empty = False
|
||||
else:
|
||||
if r.reason != 'Not Found':
|
||||
|
|
|
@ -4,11 +4,11 @@ from . import check_input_attribute, standard_error_message
|
|||
from pymisp import MISPAttribute, MISPEvent, MISPObject
|
||||
|
||||
misperrors = {'error': 'Error'}
|
||||
mispattributes = {'input': ['hostname', 'domain', "ip-src", "ip-dst", "md5", "sha1", "sha256", "url"],
|
||||
mispattributes = {'input': ['hostname', 'domain', "ip-src", "ip-dst", "md5", "sha1", "sha256", "url", "ip-src|port", "ip-dst|port"],
|
||||
'format': 'misp_standard'}
|
||||
|
||||
# possible module-types: 'expansion', 'hover' or both
|
||||
moduleinfo = {'version': '5', 'author': 'Hannah Ward',
|
||||
moduleinfo = {'version': '6', 'author': 'Hannah Ward',
|
||||
'description': 'Enrich observables with the VirusTotal v3 API',
|
||||
'module-type': ['expansion']}
|
||||
|
||||
|
@ -29,7 +29,8 @@ class VirusTotalParser:
|
|||
self.input_types_mapping = {'ip-src': self.parse_ip, 'ip-dst': self.parse_ip,
|
||||
'domain': self.parse_domain, 'hostname': self.parse_domain,
|
||||
'md5': self.parse_hash, 'sha1': self.parse_hash,
|
||||
'sha256': self.parse_hash, 'url': self.parse_url}
|
||||
'sha256': self.parse_hash, 'url': self.parse_url,
|
||||
'ip-src|port': self.parse_ip_port, 'ip-dst|port': self.parse_ip_port}
|
||||
self.proxies = None
|
||||
|
||||
@staticmethod
|
||||
|
@ -51,7 +52,11 @@ class VirusTotalParser:
|
|||
def add_vt_report(self, report: vt.Object) -> str:
|
||||
analysis = report.get('last_analysis_stats')
|
||||
total = self.get_total_analysis(analysis, report.get('known_distributors'))
|
||||
permalink = f'https://www.virustotal.com/gui/{report.type}/{report.id}'
|
||||
if report.type == 'ip_address':
|
||||
rtype = 'ip-address'
|
||||
else:
|
||||
rtype = report.type
|
||||
permalink = f'https://www.virustotal.com/gui/{rtype}/{report.id}'
|
||||
|
||||
vt_object = MISPObject('virustotal-report')
|
||||
vt_object.add_attribute('permalink', type='link', value=permalink)
|
||||
|
@ -160,6 +165,9 @@ class VirusTotalParser:
|
|||
|
||||
self.misp_event.add_object(**file_object)
|
||||
return file_object.uuid
|
||||
def parse_ip_port(self, ipport: str) -> str:
|
||||
ip = ipport.split('|')[0]
|
||||
self.parse_ip(ip)
|
||||
|
||||
def parse_ip(self, ip: str) -> str:
|
||||
ip_report = self.client.get_object(f'/ip_addresses/{ip}')
|
||||
|
|
|
@ -0,0 +1,316 @@
|
|||
import json
|
||||
import requests
|
||||
from . import check_input_attribute, standard_error_message
|
||||
from ._vulnerability_parser.vulnerability_parser import (
|
||||
VulnerabilityMapping, VulnerabilityParser)
|
||||
from pymisp import MISPObject
|
||||
from typing import Iterator
|
||||
|
||||
misperrors = {'error': 'Error'}
|
||||
mispattributes = {'input': ['vulnerability'], 'format': 'misp_standard'}
|
||||
moduleinfo = {
|
||||
'version': '1', 'author': 'Christian Studer',
|
||||
'description': 'An expansion module to query Vulnerability Lookup',
|
||||
'module-type': ['expansion', 'hover']
|
||||
}
|
||||
api_url = 'https://vulnerability.circl.lu'
|
||||
|
||||
|
||||
class VulnerabilityLookupMapping(VulnerabilityMapping):
|
||||
__csaf_mapping = {
|
||||
'id': 'id',
|
||||
'initial_release_date': 'published',
|
||||
'current_release_date': 'modified'
|
||||
}
|
||||
__cve_mapping = {
|
||||
'cveId': 'id',
|
||||
'datePublished': 'published',
|
||||
'dateUpdated': 'modified',
|
||||
'state': 'state'
|
||||
}
|
||||
__gsd_mapping = {
|
||||
'id': 'id',
|
||||
'details': 'description',
|
||||
'modified': 'modified'
|
||||
}
|
||||
__nvd_mapping = {
|
||||
'id': 'id',
|
||||
'published': 'published',
|
||||
'lastModified': 'modified'
|
||||
}
|
||||
__ossf_mapping = {
|
||||
'id': 'id',
|
||||
'summary': 'summary',
|
||||
'details': 'description',
|
||||
'published': 'published',
|
||||
'modified': 'modified'
|
||||
}
|
||||
__related_vuln_mapping = {
|
||||
'cve': 'id',
|
||||
'title': 'summary',
|
||||
'discovery_date': 'published'
|
||||
}
|
||||
__source_mapping = {
|
||||
'cve': '_parse_cve_description',
|
||||
'ghsa': '_parse_standard_description',
|
||||
'gsd': '_parse_gsd_description',
|
||||
'mal': '_parse_ossf_description',
|
||||
'pysec': '_parse_standard_description',
|
||||
'var': '_parse_variot_description'
|
||||
}
|
||||
__source_mapping.update(
|
||||
dict.fromkeys(
|
||||
(
|
||||
'cisco', 'icsa', 'icsma', 'nn', 'oxas',
|
||||
'rhba', 'rhea', 'rhsa', 'sca', 'ssa', 'wid'
|
||||
),
|
||||
'_parse_csaf_description'
|
||||
)
|
||||
)
|
||||
__standard_mapping = {
|
||||
'id': 'id',
|
||||
'details': 'description',
|
||||
'published': 'published',
|
||||
'modified': 'modified'
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def csaf_mapping(cls) -> dict:
|
||||
return cls.__csaf_mapping
|
||||
|
||||
@classmethod
|
||||
def cve_mapping(cls) -> dict:
|
||||
return cls.__cve_mapping
|
||||
|
||||
@classmethod
|
||||
def gsd_mapping(cls) -> dict:
|
||||
return cls.__gsd_mapping
|
||||
|
||||
@classmethod
|
||||
def nvd_mapping(cls) -> dict:
|
||||
return cls.__nvd_mapping
|
||||
|
||||
@classmethod
|
||||
def ossf_mapping(cls) -> dict:
|
||||
return cls.__ossf_mapping
|
||||
|
||||
@classmethod
|
||||
def related_vuln_mapping(cls) -> dict:
|
||||
return cls.__related_vuln_mapping
|
||||
|
||||
@classmethod
|
||||
def source_mapping(cls, field: str) -> str:
|
||||
return cls.__source_mapping.get(field)
|
||||
|
||||
@classmethod
|
||||
def standard_mapping(cls) -> dict:
|
||||
return cls.__standard_mapping
|
||||
|
||||
|
||||
class VulnerabilityLookupParser(VulnerabilityParser):
|
||||
def __init__(self, attribute: dict):
|
||||
super().__init__(attribute)
|
||||
self.__mapping = VulnerabilityLookupMapping
|
||||
self.__errors = []
|
||||
|
||||
@property
|
||||
def errors(self) -> list:
|
||||
return self.__errors
|
||||
|
||||
@property
|
||||
def mapping(self) -> VulnerabilityLookupMapping:
|
||||
return self.__mapping
|
||||
|
||||
def parse_lookup_result(self, lookup_result: dict):
|
||||
feature = self.mapping.source_mapping(
|
||||
self.misp_attribute.value.split('-')[0].lower()
|
||||
)
|
||||
getattr(self, feature)(lookup_result)
|
||||
|
||||
def _parse_aliases(self, aliases: list) -> Iterator[str]:
|
||||
for alias in aliases:
|
||||
query = requests.get(f"{api_url}/vulnerability/{alias}")
|
||||
if query.status_code != 200:
|
||||
self.errors.append(
|
||||
f'Unable to query related vulnerability id {alias}'
|
||||
)
|
||||
continue
|
||||
vulnerability = query.json()
|
||||
if not vulnerability:
|
||||
self.errors.append(
|
||||
f'No results for related vulnerability id{alias}'
|
||||
)
|
||||
continue
|
||||
feature = self.mapping.source_mapping(alias.split('-')[0].lower())
|
||||
yield getattr(self, feature)(vulnerability)
|
||||
|
||||
def _parse_csaf_description(self, lookup_result: dict) -> str:
|
||||
description = lookup_result['document']
|
||||
|
||||
tracking = description['tracking']
|
||||
misp_object = MISPObject('vulnerability')
|
||||
for field, relation in self.mapping.csaf_mapping().items():
|
||||
misp_object.add_attribute(relation, tracking[field])
|
||||
misp_object.add_attribute('summary', description['title'])
|
||||
for reference in description.get('references', []):
|
||||
misp_object.add_attribute('references', reference['url'])
|
||||
misp_object.add_attribute('credit', description['publisher']['name'])
|
||||
misp_object.add_reference(self.misp_attribute.uuid, 'describes')
|
||||
vulnerability_object = self.misp_event.add_object(misp_object)
|
||||
|
||||
for vulnerability in lookup_result['vulnerabilities']:
|
||||
related = MISPObject('vulnerability')
|
||||
for field, relation in self.mapping.related_vuln_mapping().items():
|
||||
if vulnerability.get(field):
|
||||
related.add_attribute(relation, vulnerability[field])
|
||||
for score in vulnerability.get('scores', []):
|
||||
cvss_v3 = score['cvss_v3']
|
||||
related.add_attribute('cvss-score', cvss_v3['baseScore'])
|
||||
related.add_attribute('cvss-string', cvss_v3['vectorString'])
|
||||
for reference in vulnerability.get('references', []):
|
||||
related.add_attribute('references', reference['url'])
|
||||
related.add_reference(vulnerability_object.uuid, 'related-to')
|
||||
related_vulnerability = self.misp_event.add_object(related)
|
||||
if vulnerability.get('cwe'):
|
||||
cwe = vulnerability['cwe']
|
||||
weakness = MISPObject('weakness')
|
||||
for field, value in cwe.items():
|
||||
weakness.add_attribute(field, value)
|
||||
weakness.add_reference(related_vulnerability.uuid, 'leads-to')
|
||||
self.misp_event.add_object(weakness)
|
||||
|
||||
return vulnerability_object.uuid
|
||||
|
||||
def _parse_cve_description(self, lookup_result: dict) -> str:
|
||||
misp_object = MISPObject('vulnerability')
|
||||
cveMetaData = lookup_result['cveMetadata']
|
||||
for field, relation in self.mapping.cve_mapping().items():
|
||||
misp_object.add_attribute(relation, cveMetaData[field])
|
||||
for reference in lookup_result['containers']['cna']['references']:
|
||||
misp_object.add_attribute('references', reference['url'])
|
||||
misp_object.add_reference(self.misp_attribute.uuid, 'related-to')
|
||||
vulnerability_object = self.misp_event.add_object(misp_object)
|
||||
return vulnerability_object.uuid
|
||||
|
||||
def _parse_cve_related_description(self, cve_description: dict) -> str:
|
||||
misp_object = MISPObject('vulnerability')
|
||||
misp_object.add_attribute(
|
||||
'id', cve_description['CVE_data_meta']['ID']
|
||||
)
|
||||
misp_object.add_attribute(
|
||||
'description',
|
||||
cve_description['description']['description_data'][0]['value']
|
||||
)
|
||||
for cvss in cve_description.get('impact', {}).get('cvss', []):
|
||||
misp_object.add_attribute('cvss-score', cvss['baseScore'])
|
||||
misp_object.add_attribute('cvss-string', cvss['vectorString'])
|
||||
for reference in misp_object.get('references', {}).get('reference_data', []):
|
||||
misp_object.add_attribute('references', reference['url'])
|
||||
return self.misp_event.add_object(misp_object).uuid
|
||||
|
||||
def _parse_gsd_description(self, lookup_result: dict) -> str:
|
||||
misp_object = MISPObject('vulnerability')
|
||||
gsd_details = lookup_result['gsd']['osvSchema']
|
||||
for field, relation in self.mapping.gsd_mapping().items():
|
||||
misp_object.add_attribute(relation, gsd_details[field])
|
||||
misp_object.add_reference(self.misp_attribute.uuid, 'related-to')
|
||||
vulnerability_object = self.misp_event.add_object(misp_object)
|
||||
|
||||
for field, values in lookup_result['namespaces'].items():
|
||||
if field == 'cve.org':
|
||||
vulnerability_object.add_reference(
|
||||
self._parse_cve_related_description(values), 'related-to'
|
||||
)
|
||||
continue
|
||||
if field == 'nvd.nist.gov' and values.get('cve'):
|
||||
vulnerability_object.add_reference(
|
||||
self._parse_nvd_related_description(values['cve']),
|
||||
'related-to'
|
||||
)
|
||||
|
||||
return vulnerability_object.uuid
|
||||
|
||||
def _parse_nvd_related_description(self, nvd_description: dict) -> str:
|
||||
misp_object = MISPObject('vulnerability')
|
||||
for field, relation in self.mapping.nvd_mapping().items():
|
||||
misp_object.add_attribute(relation, nvd_description[field])
|
||||
misp_object.add_attribute(
|
||||
'description', nvd_description['descriptions'][0]['value']
|
||||
)
|
||||
for cvss in nvd_description.get('metrics', {}).get('cvssMetricV31', []):
|
||||
misp_object.add_attribute(
|
||||
'cvss-score', cvss['cvssData']['baseScore']
|
||||
)
|
||||
misp_object.add_attribute(
|
||||
'cvss-string', cvss['cvssData']['vectorString']
|
||||
)
|
||||
for reference in nvd_description.get('references', []):
|
||||
misp_object.add_attribute('references', reference['url'])
|
||||
return self.misp_event.add_object(misp_object).uuid
|
||||
|
||||
def _parse_ossf_description(self, lookup_result: dict) -> str:
|
||||
misp_object = MISPObject('vulnerability')
|
||||
for field, relation in self.mapping.ossf_mapping().items():
|
||||
misp_object.add_attribute(relation, lookup_result[field])
|
||||
for reference in lookup_result['references']:
|
||||
misp_object.add_attribute('references', reference['url'])
|
||||
misp_object.add_reference(self.misp_attribute.uuid, 'related-to')
|
||||
vulnerability_object = self.misp_event.add_object(misp_object)
|
||||
|
||||
if lookup_result.get('aliases'):
|
||||
for vuln_uuid in self._parse_aliases(lookup_result['aliases']):
|
||||
vulnerability_object.add_reference(vuln_uuid, 'related-to')
|
||||
|
||||
return vulnerability_object.uuid
|
||||
|
||||
def _parse_standard_description(self, lookup_result: dict) -> str:
|
||||
misp_object = MISPObject('vulnerability')
|
||||
for field, relation in self.mapping.standard_mapping().items():
|
||||
misp_object.add_attribute(relation, lookup_result[field])
|
||||
for cvss in lookup_result.get('severity', []):
|
||||
misp_object.add_attribute('cvss-string', cvss['score'])
|
||||
for reference in lookup_result['references']:
|
||||
misp_object.add_attribute('references', reference['url'])
|
||||
misp_object.add_reference(self.misp_attribute.uuid, 'related-to')
|
||||
vulnerability_object = self.misp_event.add_object(misp_object)
|
||||
|
||||
if lookup_result.get('aliases'):
|
||||
for vuln_uuid in self._parse_aliases(lookup_result['aliases']):
|
||||
vulnerability_object.add_reference(vuln_uuid, 'related-to')
|
||||
|
||||
return vulnerability_object.uuid
|
||||
|
||||
|
||||
def handler(q=False):
|
||||
if q is False:
|
||||
return q
|
||||
request = json.loads(q)
|
||||
if not check_input_attribute(request.get('attribute', {})):
|
||||
return {
|
||||
'error': f'{standard_error_message}, which should contain '
|
||||
'at least a type, a value and an UUID.'
|
||||
}
|
||||
attribute = request['attribute']
|
||||
if attribute.get('type') != 'vulnerability':
|
||||
misperrors['error'] = 'Vulnerability ID missing'
|
||||
return misperrors
|
||||
lookup = requests.get(f"{api_url}/vulnerability/{attribute['value']}")
|
||||
if lookup.status_code == 200:
|
||||
vulnerability = lookup.json()
|
||||
if not vulnerability:
|
||||
misperrors['error'] = 'Non existing Vulnerability ID.'
|
||||
return misperrors
|
||||
else:
|
||||
misperrors['error'] = 'Vulnerability Lookup API not accessible.'
|
||||
return misperrors
|
||||
parser = VulnerabilityLookupParser(attribute)
|
||||
parser.parse_lookup_result(vulnerability)
|
||||
return parser.get_results()
|
||||
|
||||
|
||||
def introspection():
|
||||
return mispattributes
|
||||
|
||||
|
||||
def version():
|
||||
return moduleinfo
|
|
@ -1,212 +0,0 @@
|
|||
import json
|
||||
from pymisp import MISPAttribute, MISPEvent
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import logging
|
||||
|
||||
import vysion.client as vysion
|
||||
|
||||
import vysion.dto as dto
|
||||
from vysion.dto.util import MISPProcessor
|
||||
|
||||
misperrors = {"error": "Error"}
|
||||
mispattributes = {
|
||||
"input": [
|
||||
"email",
|
||||
"domain",
|
||||
"hostname",
|
||||
"url",
|
||||
"text",
|
||||
"btc",
|
||||
"phone-number",
|
||||
"target-org",
|
||||
],
|
||||
"format": "misp_standard",
|
||||
}
|
||||
|
||||
# possible module-types: 'expansion', 'hover' or both
|
||||
moduleinfo = {
|
||||
"version": "1",
|
||||
"author": "Byron Labs",
|
||||
"description": "Enrich observables with the Vysion API",
|
||||
"module-type": ["expansion"],
|
||||
}
|
||||
|
||||
# config fields that your code expects from the site admin
|
||||
moduleconfig = [
|
||||
"apikey",
|
||||
"event_limit",
|
||||
"proxy_host",
|
||||
"proxy_port",
|
||||
"proxy_username",
|
||||
"proxy_password",
|
||||
]
|
||||
|
||||
LOGGER = logging.getLogger("vysion")
|
||||
LOGGER.setLevel(logging.INFO)
|
||||
LOGGER.info("Starting Vysion")
|
||||
|
||||
DEFAULT_RESULTS_LIMIT = 10
|
||||
|
||||
|
||||
def get_proxy_settings(config: dict) -> dict:
|
||||
"""Returns proxy settings in the requests format.
|
||||
If no proxy settings are set, return None."""
|
||||
proxies = None
|
||||
host = config.get("proxy_host")
|
||||
port = config.get("proxy_port")
|
||||
username = config.get("proxy_username")
|
||||
password = config.get("proxy_password")
|
||||
|
||||
if host:
|
||||
if not port:
|
||||
misperrors["error"] = (
|
||||
"The vysion_proxy_host config is set, "
|
||||
"please also set the vysion_proxy_port."
|
||||
)
|
||||
raise KeyError
|
||||
parsed = urlparse(host)
|
||||
if "http" in parsed.scheme:
|
||||
scheme = "http"
|
||||
else:
|
||||
scheme = parsed.scheme
|
||||
netloc = parsed.netloc
|
||||
host = f"{netloc}:{port}"
|
||||
|
||||
if username:
|
||||
if not password:
|
||||
misperrors["error"] = (
|
||||
"The vysion_proxy_username config is set, "
|
||||
"please also set the vysion_proxy_password."
|
||||
)
|
||||
raise KeyError
|
||||
auth = f"{username}:{password}"
|
||||
host = auth + "@" + host
|
||||
|
||||
proxies = {"http": f"{scheme}://{host}", "https": f"{scheme}://{host}"}
|
||||
return proxies
|
||||
|
||||
|
||||
def parse_error(status_code: int) -> str:
|
||||
|
||||
status_mapping = {
|
||||
500: "Vysion is blind.",
|
||||
400: "Incorrect request, please check the arguments.",
|
||||
403: "You don't have enough privileges to make the request.",
|
||||
}
|
||||
|
||||
if status_code in status_mapping:
|
||||
return status_mapping[status_code]
|
||||
|
||||
return "Vysion may not be accessible."
|
||||
|
||||
|
||||
def handler(q=False):
|
||||
|
||||
if q is False:
|
||||
return False
|
||||
|
||||
request = json.loads(q)
|
||||
|
||||
if not request.get("config") or not request["config"].get("apikey"):
|
||||
misperrors["error"] = "A Vysion api key is required for this module."
|
||||
return misperrors
|
||||
|
||||
if not request.get("attribute"):
|
||||
return {
|
||||
"error": "The request is missing required attribute information, which should contain at least a type, a value, and a UUID."
|
||||
}
|
||||
|
||||
if request["attribute"]["type"] not in mispattributes["input"]:
|
||||
return {"error": "Unsupported attribute type."}
|
||||
|
||||
# event_limit = request["config"].get("event_limit")
|
||||
attribute = request["attribute"]
|
||||
proxy_settings = get_proxy_settings(request.get("config"))
|
||||
|
||||
try:
|
||||
|
||||
client = vysion.Client(
|
||||
api_key=request["config"]["apikey"],
|
||||
headers={
|
||||
"x-tool": "MISPModuleVysionExpansion",
|
||||
},
|
||||
proxy=proxy_settings["http"] if proxy_settings else None,
|
||||
)
|
||||
|
||||
LOGGER.debug(attribute)
|
||||
|
||||
misp_attribute = MISPAttribute()
|
||||
misp_attribute.from_dict(**attribute)
|
||||
|
||||
attribute_type = misp_attribute.type
|
||||
attribute_value = misp_attribute.value
|
||||
|
||||
# https://www.misp-project.org/datamodels/#types
|
||||
|
||||
LOGGER.debug(attribute_type)
|
||||
|
||||
result = None
|
||||
|
||||
if attribute_type == "email":
|
||||
result = client.find_email(attribute_value)
|
||||
elif attribute_type == "domain":
|
||||
result = client.search(attribute_value)
|
||||
elif attribute_type == "url":
|
||||
result = client.search(
|
||||
attribute_value
|
||||
) # TODO result = client.find_url(attribute_value)
|
||||
elif attribute_type == "text":
|
||||
result = client.search(attribute_value)
|
||||
elif attribute_type == "target-org":
|
||||
result = client.search(attribute_value, exact=True)
|
||||
elif attribute_type == "btc":
|
||||
result = client.search(attribute_value) # TODO
|
||||
elif attribute_type == "phone-number":
|
||||
result = client.search(attribute_value) # TODO
|
||||
|
||||
if result is None:
|
||||
return {"results": {}}
|
||||
elif isinstance(result, dto.VysionError):
|
||||
LOGGER.error(str(result))
|
||||
return {"results": {}}
|
||||
|
||||
p = MISPProcessor()
|
||||
misp_event: MISPEvent = p.process(result, ref_attribute=misp_attribute)
|
||||
|
||||
LOGGER.info("Vysion client initialized")
|
||||
|
||||
LOGGER.info("Vysion result obtained")
|
||||
|
||||
return {
|
||||
"results": {
|
||||
"Object": [
|
||||
json.loads(object.to_json()) for object in misp_event.objects
|
||||
],
|
||||
"Attribute": [
|
||||
json.loads(attribute.to_json())
|
||||
for attribute in misp_event.attributes
|
||||
],
|
||||
"Tag": [
|
||||
json.loads(tag.to_json())
|
||||
for tag in misp_event.tags
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
except vysion.APIError as ex:
|
||||
|
||||
LOGGER.error("Error in Vysion")
|
||||
LOGGER.error(ex)
|
||||
|
||||
misperrors["error"] = ex.message
|
||||
return misperrors
|
||||
|
||||
|
||||
def introspection():
|
||||
return mispattributes
|
||||
|
||||
|
||||
def version():
|
||||
moduleinfo["config"] = moduleconfig
|
||||
return moduleinfo
|
|
@ -1,59 +0,0 @@
|
|||
import json
|
||||
import base64
|
||||
|
||||
from pymisp.tools import stix
|
||||
|
||||
misperrors = {'error': 'Error'}
|
||||
userConfig = {}
|
||||
inputSource = ['file']
|
||||
|
||||
moduleinfo = {'version': '0.2', 'author': 'Hannah Ward',
|
||||
'description': 'Import some stix stuff',
|
||||
'module-type': ['import']}
|
||||
|
||||
moduleconfig = []
|
||||
|
||||
|
||||
def handler(q=False):
|
||||
# Just in case we have no data
|
||||
if q is False:
|
||||
return False
|
||||
|
||||
# The return value
|
||||
r = {'results': []}
|
||||
|
||||
# Load up that JSON
|
||||
q = json.loads(q)
|
||||
|
||||
# It's b64 encoded, so decode that stuff
|
||||
package = base64.b64decode(q.get("data")).decode('utf-8')
|
||||
|
||||
# If something really weird happened
|
||||
if not package:
|
||||
return json.dumps({"success": 0})
|
||||
|
||||
pkg = stix.load_stix(package)
|
||||
for attrib in pkg.attributes:
|
||||
r["results"].append({"values": [attrib.value], "types": [attrib.type], "categories": [attrib.category]})
|
||||
|
||||
return r
|
||||
|
||||
|
||||
def introspection():
|
||||
modulesetup = {}
|
||||
try:
|
||||
userConfig
|
||||
modulesetup['userConfig'] = userConfig
|
||||
except NameError:
|
||||
pass
|
||||
try:
|
||||
inputSource
|
||||
modulesetup['inputSource'] = inputSource
|
||||
except NameError:
|
||||
pass
|
||||
return modulesetup
|
||||
|
||||
|
||||
def version():
|
||||
moduleinfo['config'] = moduleconfig
|
||||
return moduleinfo
|
|
@ -0,0 +1,54 @@
|
|||
# MISP-module website
|
||||
|
||||
Use all modules with a dedicate website without any MISP
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## Installation
|
||||
|
||||
**It is strongly recommended to use a virtual environment**
|
||||
|
||||
If you want to know more about virtual environments, [python has you covered](https://docs.python.org/3/tutorial/venv.html)
|
||||
|
||||
```bash
|
||||
sudo apt-get install screen -y
|
||||
pip install -r requirements.txt
|
||||
git submodule init && git submodule update ## Initialize misp-objects submodule
|
||||
python3 app.py -i ## Initialize db
|
||||
```
|
||||
|
||||
Don't forget to install **misp-modules**...
|
||||
|
||||
## Config
|
||||
|
||||
Edit `config.py`
|
||||
|
||||
- `SECRET_KEY`: Secret key for the app
|
||||
|
||||
- `FLASK_URL` : url for the instance
|
||||
|
||||
- `FLASK_PORT`: port for the instance
|
||||
|
||||
- `MISP_MODULE`: url and port where misp-module is running
|
||||
|
||||
- `ADMIN_USER`: If True, config page will not be accessible
|
||||
|
||||
- `ADMIN_PASSWORD`: Password for Admin user if `ADMIN_USER` is True
|
||||
|
||||
Rename `config.cfg.sample` to `config.cfg` then edit it:
|
||||
|
||||
- `ADMIN_USER`: If True, config page will not be accessible
|
||||
|
||||
- `ADMIN_PASSWORD`: Password for Admin user if `ADMIN_USER` is True
|
||||
|
||||
## Launch
|
||||
|
||||
```bash
|
||||
./launch.sh -l
|
||||
```
|
||||
|
||||
## Admin user
|
||||
|
||||
If admin user is active, type `/login` in url to access a login page and type the password wrote in `config.py` in `ADMIN_PASSOWRD`.
|
|
@ -0,0 +1,52 @@
|
|||
from app import create_app, db
|
||||
import argparse
|
||||
from flask import render_template
|
||||
import os
|
||||
from app.utils.init_modules import create_modules_db
|
||||
|
||||
import signal
|
||||
import sys
|
||||
import subprocess
|
||||
from app.utils.utils import gen_admin_password
|
||||
|
||||
def signal_handler(sig, frame):
|
||||
path = os.path.join(os.getcwd(), "launch.sh")
|
||||
req = [path, "-ks"]
|
||||
subprocess.call(req)
|
||||
sys.exit(0)
|
||||
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("-i", "--init_db", help="Initialise the db if it not exist", action="store_true")
|
||||
parser.add_argument("-r", "--recreate_db", help="Delete and initialise the db", action="store_true")
|
||||
parser.add_argument("-d", "--delete_db", help="Delete the db", action="store_true")
|
||||
parser.add_argument("-m", "--create_module", help="Create modules in db", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
os.environ.setdefault('FLASKENV', 'development')
|
||||
|
||||
app = create_app()
|
||||
|
||||
@app.errorhandler(404)
|
||||
def error_page_not_found(e):
|
||||
return render_template('404.html'), 404
|
||||
|
||||
|
||||
if args.init_db:
|
||||
with app.app_context():
|
||||
db.create_all()
|
||||
elif args.recreate_db:
|
||||
with app.app_context():
|
||||
db.drop_all()
|
||||
db.create_all()
|
||||
elif args.delete_db:
|
||||
with app.app_context():
|
||||
db.drop_all()
|
||||
elif args.create_module:
|
||||
with app.app_context():
|
||||
create_modules_db()
|
||||
else:
|
||||
gen_admin_password()
|
||||
app.run(host=app.config.get("FLASK_URL"), port=app.config.get("FLASK_PORT"))
|
|
@ -0,0 +1,45 @@
|
|||
from flask import Flask
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from flask_wtf import CSRFProtect
|
||||
from flask_migrate import Migrate
|
||||
from flask_session import Session
|
||||
from flask_login import LoginManager
|
||||
|
||||
from conf.config import config as Config
|
||||
import os
|
||||
|
||||
|
||||
db = SQLAlchemy()
|
||||
csrf = CSRFProtect()
|
||||
migrate = Migrate()
|
||||
session = Session()
|
||||
login_manager = LoginManager()
|
||||
|
||||
def create_app():
|
||||
app = Flask(__name__)
|
||||
config_name = os.environ.get("FLASKENV")
|
||||
|
||||
app.config.from_object(Config[config_name])
|
||||
|
||||
Config[config_name].init_app(app)
|
||||
|
||||
db.init_app(app)
|
||||
csrf.init_app(app)
|
||||
migrate.init_app(app, db, render_as_batch=True)
|
||||
app.config["SESSION_SQLALCHEMY"] = db
|
||||
session.init_app(app)
|
||||
login_manager.login_view = "account.login"
|
||||
login_manager.init_app(app)
|
||||
|
||||
from .home import home_blueprint
|
||||
from .history.history import history_blueprint
|
||||
from .account.account import account_blueprint
|
||||
from .external_tools.external_tools import external_tools_blueprint
|
||||
app.register_blueprint(home_blueprint, url_prefix="/")
|
||||
app.register_blueprint(history_blueprint, url_prefix="/")
|
||||
app.register_blueprint(account_blueprint, url_prefix="/")
|
||||
app.register_blueprint(external_tools_blueprint, url_prefix="/")
|
||||
csrf.exempt(home_blueprint)
|
||||
|
||||
return app
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
from ..db_class.db import User
|
||||
from flask import Blueprint, render_template, redirect, url_for, request, flash
|
||||
from .form import LoginForm
|
||||
from flask_login import (
|
||||
login_required,
|
||||
login_user,
|
||||
logout_user,
|
||||
current_user
|
||||
)
|
||||
from ..utils.utils import admin_password
|
||||
from ..db_class.db import User
|
||||
from .. import db
|
||||
|
||||
account_blueprint = Blueprint(
|
||||
'account',
|
||||
__name__,
|
||||
template_folder='templates',
|
||||
static_folder='static'
|
||||
)
|
||||
|
||||
@account_blueprint.route('/login', methods=['GET', 'POST'])
|
||||
def login():
|
||||
"""Log in an existing user."""
|
||||
form = LoginForm()
|
||||
if form.validate_on_submit():
|
||||
if form.password.data == str(admin_password()):
|
||||
user = User(email="admin@admin.admin")
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
login_user(user, form.remember_me.data)
|
||||
flash('You are now logged in. Welcome back!', 'success')
|
||||
return redirect(request.args.get('next') or "/")
|
||||
else:
|
||||
flash('Invalid password.', 'error')
|
||||
return render_template('account/login.html', form=form)
|
||||
|
||||
@account_blueprint.route('/logout')
|
||||
@login_required
|
||||
def logout():
|
||||
User.query.filter_by(id=current_user.id).delete()
|
||||
logout_user()
|
||||
|
||||
flash('You have been logged out.', 'info')
|
||||
return redirect(url_for('home.home'))
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
from flask_wtf import FlaskForm
|
||||
from wtforms.fields import (
|
||||
BooleanField,
|
||||
PasswordField,
|
||||
SubmitField
|
||||
)
|
||||
from wtforms.validators import InputRequired
|
||||
|
||||
|
||||
class LoginForm(FlaskForm):
|
||||
password = PasswordField('Password', validators=[InputRequired()])
|
||||
remember_me = BooleanField('Keep me logged in')
|
||||
submit = SubmitField('Log in')
|
|
@ -0,0 +1,120 @@
|
|||
import json
|
||||
from .. import db, login_manager
|
||||
from flask_login import UserMixin, AnonymousUserMixin
|
||||
|
||||
|
||||
class Module(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||
name = db.Column(db.String, index=True, unique=True)
|
||||
description = db.Column(db.String)
|
||||
is_active = db.Column(db.Boolean, default=True)
|
||||
request_on_query = db.Column(db.Boolean, default=False)
|
||||
input_attr = db.Column(db.String)
|
||||
|
||||
def to_json(self):
|
||||
json_dict = {
|
||||
"id": self.id,
|
||||
"name": self.name,
|
||||
"description": self.description,
|
||||
"is_active": self.is_active,
|
||||
"request_on_query": self.request_on_query,
|
||||
"input_attr": self.input_attr
|
||||
}
|
||||
return json_dict
|
||||
|
||||
class Session_db(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||
uuid = db.Column(db.String(36), index=True, unique=True)
|
||||
modules_list = db.Column(db.String)
|
||||
query_enter = db.Column(db.String)
|
||||
input_query = db.Column(db.String)
|
||||
config_module=db.Column(db.String)
|
||||
result=db.Column(db.String)
|
||||
nb_errors = db.Column(db.Integer, index=True)
|
||||
query_date = db.Column(db.DateTime, index=True)
|
||||
|
||||
def to_json(self):
|
||||
json_dict = {
|
||||
"id": self.id,
|
||||
"uuid": self.uuid,
|
||||
"modules": json.loads(self.modules_list),
|
||||
"query_enter": json.loads(self.query_enter),
|
||||
"input_query": self.input_query,
|
||||
"config_module": json.loads(self.config_module),
|
||||
"result": json.loads(self.result),
|
||||
"nb_errors": self.nb_errors,
|
||||
"query_date": self.query_date.strftime('%Y-%m-%d %H:%M')
|
||||
}
|
||||
return json_dict
|
||||
|
||||
def history_json(self):
|
||||
json_dict = {
|
||||
"uuid": self.uuid,
|
||||
"modules": json.loads(self.modules_list),
|
||||
"query": json.loads(self.query_enter),
|
||||
"input": self.input_query,
|
||||
"query_date": self.query_date.strftime('%Y-%m-%d %H:%M')
|
||||
}
|
||||
return json_dict
|
||||
|
||||
|
||||
class History(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||
session_id = db.Column(db.Integer, index=True)
|
||||
|
||||
class History_Tree(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||
session_uuid = db.Column(db.String(36), index=True)
|
||||
tree = db.Column(db.String)
|
||||
|
||||
class Config(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||
name = db.Column(db.String, index=True, unique=True)
|
||||
|
||||
class Module_Config(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||
module_id = db.Column(db.Integer, index=True)
|
||||
config_id = db.Column(db.Integer, index=True)
|
||||
value = db.Column(db.String, index=True)
|
||||
|
||||
|
||||
class User(UserMixin, db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||
first_name = db.Column(db.String(64), index=True)
|
||||
last_name = db.Column(db.String(64), index=True)
|
||||
email = db.Column(db.String(64), unique=True, index=True)
|
||||
|
||||
def to_json(self):
|
||||
return {
|
||||
"id": self.id,
|
||||
"first_name": self.first_name,
|
||||
"last_name": self.last_name,
|
||||
"email": self.email
|
||||
}
|
||||
|
||||
class ExternalTools(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||
name = db.Column(db.String(64), index=True)
|
||||
url = db.Column(db.String)
|
||||
is_active = db.Column(db.Boolean)
|
||||
|
||||
def to_json(self):
|
||||
return {
|
||||
"id": self.id,
|
||||
"url": self.url,
|
||||
"name": self.name,
|
||||
"is_active": self.is_active
|
||||
}
|
||||
|
||||
class AnonymousUser(AnonymousUserMixin):
|
||||
def is_admin(self):
|
||||
return False
|
||||
|
||||
def read_only(self):
|
||||
return True
|
||||
|
||||
login_manager.anonymous_user = AnonymousUser
|
||||
|
||||
@login_manager.user_loader
|
||||
def load_user(user_id):
|
||||
return User.query.get(int(user_id))
|
|
@ -0,0 +1,72 @@
|
|||
import json
|
||||
from flask import Blueprint, render_template, request, jsonify, redirect, session as sess
|
||||
from ..utils.utils import admin_user_active
|
||||
from . import external_tools_core as ToolModel
|
||||
from .form import ExternalToolForm
|
||||
|
||||
external_tools_blueprint = Blueprint(
|
||||
'external_tools',
|
||||
__name__,
|
||||
template_folder='templates',
|
||||
static_folder='static'
|
||||
)
|
||||
|
||||
|
||||
@external_tools_blueprint.route("/external_tools", methods=["GET"])
|
||||
def external_tools():
|
||||
"""View config page for external tools"""
|
||||
sess["admin_user"] = admin_user_active()
|
||||
return render_template("external_tools/external_tools_index.html")
|
||||
|
||||
@external_tools_blueprint.route("/external_tools/list", methods=['GET'])
|
||||
def analyzers_data():
|
||||
"""List all tools"""
|
||||
return [tool.to_json() for tool in ToolModel.get_tools()]
|
||||
|
||||
@external_tools_blueprint.route("/add_external_tool", methods=['GET', 'POST'])
|
||||
def add_external_tool():
|
||||
"""Add a new tool"""
|
||||
form = ExternalToolForm()
|
||||
if form.validate_on_submit():
|
||||
if ToolModel.add_tool_core(ToolModel.form_to_dict(form)):
|
||||
return redirect("/external_tools")
|
||||
return render_template("external_tools/add_external_tool.html", form=form)
|
||||
|
||||
|
||||
@external_tools_blueprint.route("/external_tools/<tid>/delete_tool", methods=['GET', 'POST'])
|
||||
def delete_tool(tid):
|
||||
"""Delete a tool"""
|
||||
if ToolModel.get_tool(tid):
|
||||
if ToolModel.delete_tool(tid):
|
||||
return {"message": "Tool deleted", "toast_class": "success-subtle"}, 200
|
||||
return {"message": "Error tool deleted", 'toast_class': "danger-subtle"}, 400
|
||||
return {"message": "Tool not found", 'toast_class': "danger-subtle"}, 404
|
||||
|
||||
|
||||
|
||||
@external_tools_blueprint.route("/external_tools/change_status", methods=['GET', 'POST'])
|
||||
def change_status():
|
||||
"""Active or disabled a tool"""
|
||||
if "tool_id" in request.args:
|
||||
res = ToolModel.change_status_core(request.args.get("tool_id"))
|
||||
if res:
|
||||
return {'message': 'Tool status changed', 'toast_class': "success-subtle"}, 200
|
||||
return {'message': 'Something went wrong', 'toast_class': "danger-subtle"}, 400
|
||||
return {'message': 'Need to pass "tool_id"', 'toast_class': "warning-subtle"}, 400
|
||||
|
||||
|
||||
@external_tools_blueprint.route("/external_tools/change_config", methods=['GET', 'POST'])
|
||||
def change_config():
|
||||
"""Change configuration for a tool"""
|
||||
if "tool_id" in request.json["result_dict"] and request.json["result_dict"]["tool_id"]:
|
||||
if "tool_name" in request.json["result_dict"] and request.json["result_dict"]["tool_name"]:
|
||||
if "tool_url" in request.json["result_dict"] and request.json["result_dict"]["tool_url"]:
|
||||
res = ToolModel.change_config_core(request.json["result_dict"])
|
||||
if res:
|
||||
return {'message': 'Config changed', 'toast_class': "success-subtle"}, 200
|
||||
return {'message': 'Something went wrong', 'toast_class': "danger-subtle"}, 400
|
||||
return {'message': 'Need to pass "tool_url"', 'toast_class': "warning-subtle"}, 400
|
||||
return {'message': 'Need to pass "tool_name"', 'toast_class': "warning-subtle"}, 400
|
||||
return {'message': 'Need to pass "tool_id"', 'toast_class': "warning-subtle"}, 400
|
||||
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
from .. import db
|
||||
from ..db_class.db import *
|
||||
|
||||
|
||||
def get_tool(tool_id):
|
||||
"""Return a tool by id"""
|
||||
return ExternalTools.query.get(tool_id)
|
||||
|
||||
def get_tools():
|
||||
"""Return all External tools"""
|
||||
return ExternalTools.query.all()
|
||||
|
||||
def change_status_core(tool_id):
|
||||
"""Active or disabled a tool"""
|
||||
an = get_tool(tool_id)
|
||||
if an:
|
||||
an.is_active = not an.is_active
|
||||
db.session.commit()
|
||||
return True
|
||||
return False
|
||||
|
||||
def change_config_core(request_json):
|
||||
"""Change config for a tool"""
|
||||
tool = get_tool(request_json["tool_id"])
|
||||
if tool:
|
||||
tool.name = request_json["tool_name"]
|
||||
tool.url = request_json["tool_url"]
|
||||
db.session.commit()
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def add_tool_core(form_dict):
|
||||
tool = ExternalTools(
|
||||
name=form_dict["name"],
|
||||
url = form_dict["url"],
|
||||
is_active=True
|
||||
)
|
||||
db.session.add(tool)
|
||||
db.session.commit()
|
||||
return True
|
||||
|
||||
def delete_tool(tool_id):
|
||||
tool = get_tool(tool_id)
|
||||
if tool:
|
||||
db.session.delete(tool)
|
||||
return True
|
||||
return False
|
||||
|
||||
def form_to_dict(form):
|
||||
loc_dict = dict()
|
||||
for field in form._fields:
|
||||
if field == "files_upload":
|
||||
loc_dict[field] = dict()
|
||||
loc_dict[field]["data"] = form._fields[field].data
|
||||
loc_dict[field]["name"] = form._fields[field].name
|
||||
elif not field == "submit" and not field == "csrf_token":
|
||||
loc_dict[field] = form._fields[field].data
|
||||
return loc_dict
|
|
@ -0,0 +1,12 @@
|
|||
from flask_wtf import FlaskForm
|
||||
from wtforms.fields import (
|
||||
StringField,
|
||||
SubmitField,
|
||||
)
|
||||
from wtforms.validators import InputRequired, Length
|
||||
|
||||
|
||||
class ExternalToolForm(FlaskForm):
|
||||
name = StringField('Name', validators=[InputRequired(), Length(1, 64)])
|
||||
url = StringField('Url', validators=[InputRequired()])
|
||||
submit = SubmitField('Create')
|
|
@ -0,0 +1,90 @@
|
|||
import json
|
||||
from flask import Flask, Blueprint, render_template, request, jsonify, session as sess
|
||||
from . import history_core as HistoryModel
|
||||
from ..utils.utils import admin_user_active
|
||||
|
||||
history_blueprint = Blueprint(
|
||||
'history',
|
||||
__name__,
|
||||
template_folder='templates',
|
||||
static_folder='static'
|
||||
)
|
||||
|
||||
|
||||
@history_blueprint.route("/history", methods=["GET"])
|
||||
def history():
|
||||
"""View all history"""
|
||||
sess["admin_user"] = admin_user_active()
|
||||
return render_template("history.html")
|
||||
|
||||
@history_blueprint.route("/get_history", methods=["GET"])
|
||||
def get_history():
|
||||
"""Get all history"""
|
||||
page = request.args.get('page', 1, type=int)
|
||||
histories, nb_pages = HistoryModel.get_history(page)
|
||||
return {"history": histories, "nb_pages": nb_pages}
|
||||
|
||||
@history_blueprint.route("/history_session", methods=["GET"])
|
||||
def history_session():
|
||||
"""View all history"""
|
||||
sess["admin_user"] = admin_user_active()
|
||||
return render_template("history_session.html", tree_view=False)
|
||||
|
||||
@history_blueprint.route("/get_history_session", methods=["GET"])
|
||||
def get_history_session():
|
||||
"""Get all history"""
|
||||
histories = HistoryModel.get_history_session()
|
||||
if histories:
|
||||
return histories
|
||||
return {}
|
||||
|
||||
@history_blueprint.route("/get_current_query_history", methods=["GET"])
|
||||
def get_current_query_history():
|
||||
"""Get current query history"""
|
||||
return HistoryModel.get_current_query_history()
|
||||
|
||||
|
||||
@history_blueprint.route("/save_history/<sid>", methods=["GET"])
|
||||
def save_history(sid):
|
||||
return HistoryModel.save_history_core(sid)
|
||||
|
||||
|
||||
@history_blueprint.route("/history_tree", methods=["GET"])
|
||||
def history_tree():
|
||||
"""View all history"""
|
||||
sess["admin_user"] = admin_user_active()
|
||||
return render_template("history_session.html", tree_view=True)
|
||||
|
||||
@history_blueprint.route("/get_history_tree", methods=["GET"])
|
||||
def get_history_tree():
|
||||
"""Get all history"""
|
||||
histories = HistoryModel.get_history_tree()
|
||||
if histories:
|
||||
return histories
|
||||
return {}
|
||||
|
||||
@history_blueprint.route("/get_history_tree/<sid>", methods=["GET"])
|
||||
def get_history_tree_uuid(sid):
|
||||
"""Get all history"""
|
||||
histories = HistoryModel.get_history_tree_uuid(sid)
|
||||
if histories:
|
||||
return histories
|
||||
return {}
|
||||
|
||||
@history_blueprint.route("/get_history_session/<sid>", methods=["GET"])
|
||||
def get_history_session_uuid(sid):
|
||||
"""Get all history"""
|
||||
histories = HistoryModel.get_history_session_uuid(sid)
|
||||
if histories:
|
||||
return histories
|
||||
return {}
|
||||
|
||||
@history_blueprint.route("/history/remove_node_session/<sid>", methods=["GET"])
|
||||
def remove_node_session(sid):
|
||||
HistoryModel.remove_node_session(sid)
|
||||
return {"message": "Node deleted", "toast_class": "success-subtle"}
|
||||
|
||||
@history_blueprint.route("/history/remove_node_tree/<sid>", methods=["GET"])
|
||||
def remove_node_tree(sid):
|
||||
HistoryModel.remove_node_tree(sid)
|
||||
return {"message": "Node deleted", "toast_class": "success-subtle"}
|
|
@ -0,0 +1,196 @@
|
|||
import json
|
||||
from ..utils.utils import isUUID
|
||||
from .. import db
|
||||
from ..db_class.db import History, Session_db, History_Tree
|
||||
from flask import session as sess
|
||||
from sqlalchemy import desc
|
||||
|
||||
|
||||
|
||||
def get_session(sid):
|
||||
"""Return a session by uuid"""
|
||||
return Session_db.query.filter_by(uuid=sid).first()
|
||||
|
||||
|
||||
|
||||
def get_history(page):
|
||||
"""Return history"""
|
||||
histories_list = list()
|
||||
histories = History.query.order_by(desc(History.id)).paginate(page=page, per_page=20, max_per_page=50)
|
||||
for history in histories:
|
||||
session = Session_db.query.get(history.session_id)
|
||||
histories_list.append(session.history_json())
|
||||
return histories_list, histories.pages
|
||||
|
||||
|
||||
def get_history_session():
|
||||
current_query = sess.get("current_query")
|
||||
loc_list = list()
|
||||
if current_query:
|
||||
# If current query have no children then don't display it
|
||||
# It's already save in history
|
||||
# Only parent-child tree structure is in flask session
|
||||
current_query_value = sess.get(current_query)
|
||||
if current_query_value:
|
||||
loc_list.append(current_query_value)
|
||||
for q in sess:
|
||||
if isUUID(q):
|
||||
# If query have no children then don't display it
|
||||
q_value = sess.get(q)
|
||||
if not q == current_query:
|
||||
loc_list.append(q_value)
|
||||
|
||||
return loc_list
|
||||
|
||||
def get_current_query_history():
|
||||
current_query = sess.get("current_query")
|
||||
if current_query:
|
||||
current_query_value = sess.get(current_query)
|
||||
if current_query_value:
|
||||
return current_query_value
|
||||
return {}
|
||||
|
||||
|
||||
def get_history_session_uuid(history_uuid):
|
||||
for q in sess:
|
||||
if isUUID(q):
|
||||
# If query have no children then don't display it
|
||||
q_value = sess.get(q)
|
||||
if q == history_uuid:
|
||||
return q_value
|
||||
return {}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def util_save_history(session):
|
||||
loc_dict = dict()
|
||||
loc_dict[session["uuid"]] = []
|
||||
|
||||
if "children" in session and session["children"]:
|
||||
for child in session["children"]:
|
||||
loc_dict[session["uuid"]].append(util_save_history(child))
|
||||
return loc_dict
|
||||
|
||||
|
||||
def save_history_core(sid):
|
||||
"""Save history from session to db"""
|
||||
if sid in sess:
|
||||
session = sess.get(sid)
|
||||
# Doesn't already exist
|
||||
history_tree_db = History_Tree.query.filter_by(session_uuid=session["uuid"]).first()
|
||||
if not history_tree_db:
|
||||
# Get all children before add to db
|
||||
loc_dict = util_save_history(session)
|
||||
h = History_Tree(
|
||||
session_uuid = session["uuid"],
|
||||
tree=json.dumps(loc_dict)
|
||||
)
|
||||
db.session.add(h)
|
||||
db.session.commit()
|
||||
return {"message": "History Save", 'toast_class': "success-subtle"}
|
||||
# Save same session but with new value
|
||||
elif not json.loads(history_tree_db.tree) == session:
|
||||
# Get all children before add to db
|
||||
loc_dict = util_save_history(session)
|
||||
history_tree_db.tree = json.dumps(loc_dict)
|
||||
db.session.commit()
|
||||
return {"message": "History updated", 'toast_class': "success-subtle"}
|
||||
return {"message": "History already saved", 'toast_class': "warning-subtle"}
|
||||
return {"message": "Session not found", 'toast_class': "danger-subtle"}
|
||||
|
||||
|
||||
|
||||
def util_get_history_tree(child):
|
||||
loc_child = list(child.keys())[0]
|
||||
loc_session = get_session(loc_child)
|
||||
loc_json = loc_session.history_json()
|
||||
loc_json["children"] = list()
|
||||
if child[loc_child]:
|
||||
for s_child in child[loc_child]:
|
||||
loc_json["children"].append(util_get_history_tree(s_child))
|
||||
return loc_json
|
||||
|
||||
def get_history_tree():
|
||||
"""Return all histories saved as tree"""
|
||||
histories_tree = History_Tree.query.order_by(desc(History_Tree.id))
|
||||
loc_dict = list()
|
||||
for history_tree in histories_tree:
|
||||
tree = json.loads(history_tree.tree)
|
||||
loc_session = get_session(history_tree.session_uuid)
|
||||
loc_json = loc_session.history_json()
|
||||
loc_json["children"] = list()
|
||||
for child in tree[history_tree.session_uuid]:
|
||||
loc_json["children"].append(util_get_history_tree(child))
|
||||
loc_dict.append(loc_json)
|
||||
return loc_dict
|
||||
|
||||
|
||||
|
||||
def get_history_tree_uuid(history_uuid):
|
||||
history_tree = History_Tree.query.filter_by(session_uuid=history_uuid).first()
|
||||
if history_tree:
|
||||
tree = json.loads(history_tree.tree)
|
||||
loc_session = get_session(history_tree.session_uuid)
|
||||
loc_json = loc_session.history_json()
|
||||
loc_json["children"] = list()
|
||||
for child in tree[history_tree.session_uuid]:
|
||||
loc_json["children"].append(util_get_history_tree(child))
|
||||
return loc_json
|
||||
return {}
|
||||
|
||||
|
||||
def util_remove_node_session(node_uuid, parent, parent_path):
|
||||
for i in range(0, len(parent["children"])):
|
||||
child = parent["children"][i]
|
||||
if child["uuid"] == node_uuid:
|
||||
del parent_path["children"][i]
|
||||
return True
|
||||
elif "children" in child and child["children"]:
|
||||
return util_remove_node_session(node_uuid, child, parent_path["children"][i])
|
||||
|
||||
def remove_node_session(node_uuid):
|
||||
keys_list = list(sess.keys())
|
||||
loc = None
|
||||
for i in range(0, len(keys_list)):
|
||||
if isUUID(keys_list[i]):
|
||||
q_value = sess.get(keys_list[i])
|
||||
if q_value["uuid"] == node_uuid:
|
||||
loc = i
|
||||
break
|
||||
elif q_value["children"]:
|
||||
if util_remove_node_session(node_uuid, q_value, sess[keys_list[i]]):
|
||||
loc = i
|
||||
break
|
||||
if loc:
|
||||
del sess[keys_list[i]]
|
||||
|
||||
|
||||
|
||||
def util_remove_node_tree(node_uuid, parent, parent_path):
|
||||
for i in range(0, len(parent)):
|
||||
child = parent[i]
|
||||
for key in child:
|
||||
if key == node_uuid:
|
||||
del parent_path[i]
|
||||
return
|
||||
elif parent[i][key]:
|
||||
return util_remove_node_tree(node_uuid, parent[i][key], parent[i][key])
|
||||
|
||||
|
||||
def remove_node_tree(node_uuid):
|
||||
histories_tree = History_Tree.query.order_by(desc(History_Tree.id))
|
||||
for history_tree in histories_tree:
|
||||
tree = json.loads(history_tree.tree)
|
||||
for e in tree:
|
||||
if e == node_uuid:
|
||||
db.session.delete(history_tree)
|
||||
db.session.commit()
|
||||
return
|
||||
else:
|
||||
if tree[e]:
|
||||
util_remove_node_tree(node_uuid, tree[e], tree[e])
|
||||
history_tree.tree = json.dumps(tree)
|
||||
db.session.commit()
|
||||
return
|
|
@ -0,0 +1,257 @@
|
|||
import ast
|
||||
import json
|
||||
from flask import Blueprint, render_template, request, jsonify, session as sess
|
||||
from flask_login import current_user
|
||||
from . import session_class as SessionModel
|
||||
from . import home_core as HomeModel
|
||||
from .utils.utils import admin_user_active
|
||||
|
||||
home_blueprint = Blueprint(
|
||||
'home',
|
||||
__name__,
|
||||
template_folder='templates',
|
||||
static_folder='static'
|
||||
)
|
||||
|
||||
|
||||
@home_blueprint.route("/", methods=["GET", "POST"])
|
||||
def home():
|
||||
try:
|
||||
del sess["query"]
|
||||
except:
|
||||
pass
|
||||
sess["admin_user"] = bool(admin_user_active())
|
||||
if "query" in request.args:
|
||||
sess["query"] = ast.literal_eval(request.args.get("query"))
|
||||
if "query" in request.form:
|
||||
sess["query"] = json.loads(request.form.get("query"))
|
||||
return render_template("home.html")
|
||||
|
||||
@home_blueprint.route("/get_query", methods=['GET', 'POST'])
|
||||
def get_query():
|
||||
"""Get result from flowintel"""
|
||||
if "query" in sess:
|
||||
return {"query": sess.get("query")}
|
||||
return {"message": "No query"}
|
||||
|
||||
@home_blueprint.route("/home/<sid>", methods=["GET", "POST"])
|
||||
def home_query(sid):
|
||||
try:
|
||||
del sess["query"]
|
||||
except:
|
||||
pass
|
||||
sess["admin_user"] = admin_user_active()
|
||||
if "query" in request.args:
|
||||
sess["query"] = [request.args.get("query")]
|
||||
return render_template("home.html", query=query, sid=sid)
|
||||
return render_template("404.html")
|
||||
|
||||
@home_blueprint.route("/query/<sid>")
|
||||
def query(sid):
|
||||
sess["admin_user"] = admin_user_active()
|
||||
session = HomeModel.get_session(sid)
|
||||
flag=False
|
||||
modules_list = []
|
||||
if session:
|
||||
flag = True
|
||||
query_loc = json.loads(session.query_enter)
|
||||
modules_list = json.loads(session.modules_list)
|
||||
else:
|
||||
for s in SessionModel.sessions:
|
||||
if s.uuid == sid:
|
||||
flag = True
|
||||
query_loc = s.query
|
||||
session=s
|
||||
modules_list = session.modules_list
|
||||
query_str = ", ".join(query_loc)
|
||||
if len(query_str) > 40:
|
||||
query_str = query_str[0:40] + "..."
|
||||
if flag:
|
||||
return render_template("query.html",
|
||||
query=query_loc,
|
||||
query_str=query_str,
|
||||
sid=sid,
|
||||
input_query=session.input_query,
|
||||
modules=modules_list,
|
||||
query_date=session.query_date.strftime('%Y-%m-%d %H:%M'))
|
||||
return render_template("404.html")
|
||||
|
||||
|
||||
|
||||
@home_blueprint.route("/get_query_info/<sid>")
|
||||
def get_query_info(sid):
|
||||
"""Return info for a query"""
|
||||
session = HomeModel.get_session(sid)
|
||||
flag=False
|
||||
if session:
|
||||
flag = True
|
||||
query_loc = json.loads(session.query_enter)
|
||||
modules_list = json.loads(session.modules_list)
|
||||
else:
|
||||
for s in SessionModel.sessions:
|
||||
if s.uuid == sid:
|
||||
flag = True
|
||||
query_loc = s.query
|
||||
modules_list = s.modules_list
|
||||
session=s
|
||||
if flag:
|
||||
loc_dict = {
|
||||
"query": query_loc,
|
||||
"input_query": session.input_query,
|
||||
"modules": modules_list,
|
||||
"query_date": session.query_date.strftime('%Y-%m-%d %H:%M')
|
||||
}
|
||||
return loc_dict
|
||||
return {"message": "Session not found"}, 404
|
||||
|
||||
|
||||
@home_blueprint.route("/get_modules")
|
||||
def get_modules():
|
||||
"""Return all modules available"""
|
||||
res = HomeModel.get_modules()
|
||||
|
||||
if "message" in res:
|
||||
return res, 404
|
||||
return res, 200
|
||||
|
||||
@home_blueprint.route("/get_list_misp_attributes")
|
||||
def get_list_misp_attributes():
|
||||
"""Return all misp attributes for input and output"""
|
||||
res = HomeModel.get_list_misp_attributes()
|
||||
|
||||
if "message" in res:
|
||||
return res, 404
|
||||
return res, 200
|
||||
|
||||
@home_blueprint.route("/run_modules", methods=['POST'])
|
||||
def run_modules():
|
||||
"""Run modules"""
|
||||
if "query" in request.json:
|
||||
if "input" in request.json and request.json["input"]:
|
||||
if "modules" in request.json:
|
||||
if "query_as_same" in request.json:
|
||||
session = SessionModel.Session_class(request.json, query_as_same=True, parent_id=request.json["parent_id"])
|
||||
elif "query_as_params" in request.json:
|
||||
session = SessionModel.Session_class(request.json, query_as_same=True, parent_id=request.json["same_query_id"])
|
||||
else:
|
||||
session = SessionModel.Session_class(request.json)
|
||||
HomeModel.set_flask_session(session, request.json["parent_id"])
|
||||
session.start()
|
||||
SessionModel.sessions.append(session)
|
||||
return jsonify(session.status()), 201
|
||||
return {"message": "Need a module type"}, 400
|
||||
return {"message": "Need an input (misp attribute)"}, 400
|
||||
return {"message": "Need to type something"}, 400
|
||||
|
||||
@home_blueprint.route("/status/<sid>")
|
||||
def status(sid):
|
||||
"""Status of <sid> queue"""
|
||||
sess = HomeModel.get_session(sid)
|
||||
if sess:
|
||||
return jsonify(HomeModel.get_status_db(sess))
|
||||
else:
|
||||
for s in SessionModel.sessions:
|
||||
if s.uuid == sid:
|
||||
return jsonify(s.status())
|
||||
return jsonify({'message': 'Scan session not found'}), 404
|
||||
|
||||
@home_blueprint.route("/result/<sid>")
|
||||
def result(sid):
|
||||
"""Result of <sid> queue"""
|
||||
sess = HomeModel.get_session(sid)
|
||||
if sess:
|
||||
return jsonify(HomeModel.get_result_db(sess))
|
||||
else:
|
||||
for s in SessionModel.sessions:
|
||||
if s.uuid == sid:
|
||||
return jsonify(s.get_result())
|
||||
return jsonify({'message': 'Scan session not found'}), 404
|
||||
|
||||
|
||||
|
||||
@home_blueprint.route("/download/<sid>")
|
||||
def download(sid):
|
||||
"""Download a module result as json"""
|
||||
sess = HomeModel.get_session(sid)
|
||||
if "module" in request.args:
|
||||
if sess:
|
||||
loc = json.loads(sess.result)
|
||||
module = request.args.get("module")
|
||||
if module in loc:
|
||||
return jsonify(loc[module]), 200, {'Content-Disposition': f'attachment; filename={sess.query_enter.replace(".", "_")}-{module}.json'}
|
||||
return {"message": "Module not in result", "toast_class": "danger-subtle"}, 400
|
||||
else:
|
||||
for s in SessionModel.sessions:
|
||||
if s.uuid == sid:
|
||||
module = request.args.get("module")
|
||||
if module in s.result:
|
||||
return jsonify(s.result[module]), 200, {'Content-Disposition': f'attachment; filename={s.query}-{module}.json'}
|
||||
return {"message": "Module not in result", "toast_class": "danger-subtle"}, 400
|
||||
return {"message": "Session not found", 'toast_class': "danger-subtle"}, 404
|
||||
return {"message": "Need to pass a module", "toast_class": "warning-subtle"}, 400
|
||||
|
||||
|
||||
|
||||
|
||||
@home_blueprint.route("/modules_config")
|
||||
def modules_config():
|
||||
"""List all modules for configuration"""
|
||||
sess["admin_user"] = admin_user_active()
|
||||
flag = True
|
||||
if sess.get("admin_user"):
|
||||
if not current_user.is_authenticated:
|
||||
flag = False
|
||||
if flag:
|
||||
return render_template("modules_config.html")
|
||||
return render_template("404.html")
|
||||
|
||||
|
||||
@home_blueprint.route("/modules_config_data")
|
||||
def modules_config_data():
|
||||
"""List all modules for configuration"""
|
||||
sess["admin_user"] = admin_user_active()
|
||||
flag = True
|
||||
if sess.get("admin_user"):
|
||||
if not current_user.is_authenticated:
|
||||
flag = False
|
||||
if flag:
|
||||
modules_config = HomeModel.get_modules_config()
|
||||
return modules_config, 200
|
||||
return {"message": "Permission denied"}, 403
|
||||
|
||||
|
||||
@home_blueprint.route("/change_config", methods=["POST"])
|
||||
def change_config():
|
||||
"""Change configuation for a module"""
|
||||
sess["admin_user"] = admin_user_active()
|
||||
flag = True
|
||||
if sess.get("admin_user"):
|
||||
if not current_user.is_authenticated:
|
||||
flag = False
|
||||
if flag:
|
||||
if "module_name" in request.json["result_dict"]:
|
||||
res = HomeModel.change_config_core(request.json["result_dict"])
|
||||
if res:
|
||||
return {'message': 'Config changed', 'toast_class': "success-subtle"}, 200
|
||||
return {'message': 'Something went wrong', 'toast_class': "danger-subtle"}, 400
|
||||
return {'message': 'Need to pass "module_name"', 'toast_class': "warning-subtle"}, 400
|
||||
return {'message': 'Permission denied', 'toast_class': "danger-subtle"}, 403
|
||||
|
||||
@home_blueprint.route("/change_status", methods=["GET"])
|
||||
def change_status():
|
||||
"""Change the status of a module, active or unactive"""
|
||||
sess["admin_user"] = admin_user_active()
|
||||
flag = True
|
||||
if sess.get("admin_user"):
|
||||
if not current_user.is_authenticated:
|
||||
flag = False
|
||||
# if admin is active and user is logon or if admin is not active
|
||||
if flag:
|
||||
if "module_id" in request.args:
|
||||
res = HomeModel.change_status_core(request.args.get("module_id"))
|
||||
if res:
|
||||
return {'message': 'Module status changed', 'toast_class': "success-subtle"}, 200
|
||||
return {'message': 'Something went wrong', 'toast_class': "danger-subtle"}, 400
|
||||
return {'message': 'Need to pass "module_id"', 'toast_class': "warning-subtle"}, 400
|
||||
return {'message': 'Permission denied', 'toast_class': "danger-subtle"}, 403
|
||||
|
|
@ -0,0 +1,235 @@
|
|||
import json
|
||||
from .utils.utils import isUUID, query_get_module
|
||||
from . import db
|
||||
from .db_class.db import History, Module, Config, Module_Config, Session_db, History_Tree
|
||||
from flask import session as sess
|
||||
from sqlalchemy import desc
|
||||
|
||||
|
||||
def get_module(mid):
|
||||
"""Return a module by id"""
|
||||
return Module.query.get(mid)
|
||||
|
||||
def get_module_by_name(name):
|
||||
"""Return a module by name"""
|
||||
return Module.query.filter_by(name=name).first()
|
||||
|
||||
def get_config(cid):
|
||||
"""Return a config by id"""
|
||||
return Config.query.get(cid)
|
||||
|
||||
def get_config_by_name(name):
|
||||
"""Return a config by name"""
|
||||
return Config.query.filter_by(name=name).first()
|
||||
|
||||
def get_module_config_module(mid):
|
||||
"""Return a moudle_config by module id"""
|
||||
return Module_Config.query.filter_by(module_id=mid).all()
|
||||
|
||||
def get_module_config_both(mid, cid):
|
||||
"""Return a moudle_config by module id and config id"""
|
||||
return Module_Config.query.filter_by(module_id=mid, config_id=cid).first()
|
||||
|
||||
def get_session(sid):
|
||||
"""Return a session by uuid"""
|
||||
return Session_db.query.filter_by(uuid=sid).first()
|
||||
|
||||
def get_modules():
|
||||
"""Return all modules for expansion and hover types"""
|
||||
res = query_get_module()
|
||||
if not "message" in res:
|
||||
loc_list = list()
|
||||
for module in res:
|
||||
module_db = get_module_by_name(module["name"])
|
||||
module_loc = module
|
||||
module_loc["request_on_query"] = module_db.request_on_query
|
||||
if module_db.is_active:
|
||||
if "expansion" in module["meta"]["module-type"] or "hover" in module["meta"]["module-type"]:
|
||||
if not module_loc in loc_list:
|
||||
loc_list.append(module_loc)
|
||||
loc_list.sort(key=lambda x: x["name"])
|
||||
return loc_list
|
||||
return res
|
||||
|
||||
|
||||
def util_get_attr(module, loc_list):
|
||||
"""Additional algo for get_list_misp_attributes"""
|
||||
if "input" in module["mispattributes"]:
|
||||
for input in module["mispattributes"]["input"]:
|
||||
if not input in loc_list:
|
||||
loc_list.append(input)
|
||||
return loc_list
|
||||
|
||||
def get_list_misp_attributes():
|
||||
"""Return all types of attributes used in expansion and hover"""
|
||||
res = query_get_module()
|
||||
if not "message" in res:
|
||||
loc_list = list()
|
||||
|
||||
for module in res:
|
||||
if get_module_by_name(module["name"]).is_active:
|
||||
if "expansion" in module["meta"]["module-type"] or "hover" in module["meta"]["module-type"]:
|
||||
loc_list = util_get_attr(module, loc_list)
|
||||
loc_list.sort()
|
||||
return loc_list
|
||||
return res
|
||||
|
||||
|
||||
def get_modules_config():
|
||||
"""Return configs for all modules """
|
||||
modules = Module.query.order_by(Module.name).all()
|
||||
modules_list = []
|
||||
for module in modules:
|
||||
loc_module = module.to_json()
|
||||
if loc_module["input_attr"]:
|
||||
loc_module["input_attr"] = json.loads(loc_module["input_attr"])
|
||||
loc_module["config"] = []
|
||||
mcs = Module_Config.query.filter_by(module_id=module.id).all()
|
||||
for mc in mcs:
|
||||
conf = Config.query.get(mc.config_id)
|
||||
loc_module["config"].append({conf.name: mc.value})
|
||||
modules_list.append(loc_module)
|
||||
return modules_list
|
||||
|
||||
|
||||
def change_config_core(request_json):
|
||||
"""Change config for a module"""
|
||||
module = get_module_by_name(request_json["module_name"])
|
||||
for element in request_json:
|
||||
if not element == "module_name":
|
||||
config = get_config_by_name(element)
|
||||
if config:
|
||||
m_c = get_module_config_both(module.id, config.id)
|
||||
m_c.value = request_json[element]
|
||||
db.session.commit()
|
||||
module.request_on_query = request_json["request_on_query"]
|
||||
db.session.commit()
|
||||
return True
|
||||
|
||||
def change_status_core(module_id):
|
||||
"""Active or deactive a module"""
|
||||
module = get_module(module_id)
|
||||
module.is_active = not module.is_active
|
||||
db.session.commit()
|
||||
return True
|
||||
|
||||
|
||||
|
||||
##############
|
||||
# Session DB #
|
||||
##############
|
||||
|
||||
def get_status_db(session):
|
||||
"""Return status of a session"""
|
||||
modules_list = json.loads(session.modules_list)
|
||||
result = json.loads(session.result)
|
||||
return{
|
||||
'id': session.uuid,
|
||||
'total': len(modules_list),
|
||||
'complete': len(modules_list),
|
||||
'remaining': 0,
|
||||
'registered': len(result),
|
||||
'stopped' : True,
|
||||
"nb_errors": session.nb_errors
|
||||
}
|
||||
|
||||
def get_result_db(session):
|
||||
"""Return result of a session"""
|
||||
return json.loads(session.result)
|
||||
|
||||
def get_history():
|
||||
"""Return history"""
|
||||
histories_list = list()
|
||||
histories = History.query.order_by(desc(History.id))
|
||||
for history in histories:
|
||||
session = Session_db.query.get(history.session_id)
|
||||
histories_list.append(session.history_json())
|
||||
return histories_list
|
||||
|
||||
|
||||
|
||||
def create_new_session_tree(current_session, parent_id):
|
||||
loc_session = get_session(parent_id)
|
||||
|
||||
loc_json_child = {
|
||||
"uuid": current_session.uuid,
|
||||
"modules": current_session.modules_list,
|
||||
"query": current_session.query,
|
||||
"input": current_session.input_query,
|
||||
"query_date": current_session.query_date.strftime('%Y-%m-%d'),
|
||||
"config": current_session.config_module,
|
||||
"children": list()
|
||||
}
|
||||
loc_json = {
|
||||
"uuid": loc_session.uuid,
|
||||
"modules": json.loads(loc_session.modules_list),
|
||||
"query": json.loads(loc_session.query_enter),
|
||||
"input": loc_session.input_query,
|
||||
"query_date": loc_session.query_date.strftime('%Y-%m-%d %H:%M'),
|
||||
"config": json.loads(loc_session.config_module),
|
||||
"children" : [loc_json_child]
|
||||
}
|
||||
|
||||
sess["current_query"] = loc_session.uuid
|
||||
sess[sess.get("current_query")] = loc_json
|
||||
|
||||
def util_set_flask_session(parent_id, loc_session, current_session):
|
||||
if parent_id == loc_session["uuid"]:
|
||||
loc_json = {
|
||||
"uuid": current_session.uuid,
|
||||
"modules": current_session.modules_list,
|
||||
"query": current_session.query,
|
||||
"input": current_session.input_query,
|
||||
"query_date": current_session.query_date.strftime('%Y-%m-%d %H:%M'),
|
||||
"config": current_session.config_module
|
||||
}
|
||||
loc_session["children"].append(loc_json)
|
||||
return True
|
||||
elif "children" in loc_session:
|
||||
return deep_explore(loc_session["children"], parent_id, current_session)
|
||||
|
||||
def deep_explore(session_dict, parent_id, current_session):
|
||||
for loc_session in session_dict:
|
||||
if not "children" in loc_session:
|
||||
loc_session["children"] = list()
|
||||
if util_set_flask_session(parent_id, loc_session, current_session):
|
||||
return True
|
||||
return False
|
||||
|
||||
def set_flask_session(current_session, parent_id):
|
||||
if parent_id:
|
||||
current_query = sess.get("current_query")
|
||||
if not current_query or current_query not in sess:
|
||||
create_new_session_tree(current_session, parent_id)
|
||||
else:
|
||||
## Check in current query
|
||||
loc_session = sess.get(current_query)
|
||||
if not "children" in loc_session:
|
||||
loc_session["children"] = list()
|
||||
## If not in current query, current query change for an other one
|
||||
if not util_set_flask_session(parent_id, loc_session, current_session):
|
||||
# sess["uuid"]
|
||||
for q in sess:
|
||||
if isUUID(q) and not q == current_query:
|
||||
loc_session = sess.get(q)
|
||||
if not "children" in loc_session:
|
||||
loc_session["children"] = list()
|
||||
if util_set_flask_session(parent_id, loc_session, current_session):
|
||||
sess["current_query"] = q
|
||||
flag = False
|
||||
break
|
||||
if flag:
|
||||
create_new_session_tree(current_session, parent_id)
|
||||
else:
|
||||
loc_json = {
|
||||
"uuid": current_session.uuid,
|
||||
"modules": current_session.modules_list,
|
||||
"query": current_session.query,
|
||||
"input": current_session.input_query,
|
||||
"query_date": current_session.query_date.strftime('%Y-%m-%d %H:%M'),
|
||||
"config": current_session.config_module,
|
||||
"children": list()
|
||||
}
|
||||
|
||||
sess["current_query"] = current_session.uuid
|
||||
sess[sess.get("current_query")] = loc_json
|
|
@ -0,0 +1,195 @@
|
|||
import datetime
|
||||
import json
|
||||
from queue import Queue
|
||||
from threading import Thread
|
||||
from uuid import uuid4
|
||||
from .utils.utils import query_post_query, query_get_module, get_object, get_limit_queries
|
||||
from . import home_core as HomeModel
|
||||
import uuid
|
||||
from . import db
|
||||
from .db_class.db import History, History_Tree, Session_db
|
||||
from flask import session as sess
|
||||
|
||||
sessions = list()
|
||||
|
||||
class Session_class:
|
||||
def __init__(self, request_json, query_as_same=False, parent_id=None) -> None:
|
||||
self.uuid = str(uuid4())
|
||||
self.thread_count = 4
|
||||
self.jobs = Queue(maxsize=0)
|
||||
self.threads = []
|
||||
self.stopped = False
|
||||
self.result_stopped = dict()
|
||||
self.result = dict()
|
||||
self.query = request_json["query"]
|
||||
self.input_query = request_json["input"]
|
||||
self.modules_list = request_json["modules"]
|
||||
self.nb_errors = 0
|
||||
self.config_module = self.config_module_setter(request_json, query_as_same, parent_id)
|
||||
self.query_date = datetime.datetime.now(tz=datetime.timezone.utc)
|
||||
|
||||
|
||||
def util_config_as_same(self, child, parent_id):
|
||||
if child["uuid"] == parent_id:
|
||||
return child["config"]
|
||||
elif "children" in child:
|
||||
for c in child["children"]:
|
||||
return self.util_config_as_same(c, parent_id)
|
||||
|
||||
|
||||
def config_module_setter(self, request_json, query_as_same, parent_id):
|
||||
"""Setter for config for all modules used"""
|
||||
flag = False
|
||||
if query_as_same:
|
||||
current_query_val = sess.get(sess.get("current_query"))
|
||||
if current_query_val:
|
||||
if current_query_val["uuid"] == parent_id:
|
||||
return current_query_val["config"]
|
||||
else:
|
||||
for child in current_query_val["children"]:
|
||||
res = self.util_config_as_same(child, parent_id)
|
||||
if res:
|
||||
flag = True
|
||||
return res
|
||||
if not flag:
|
||||
for query in self.modules_list:
|
||||
if not query in request_json["config"]:
|
||||
request_json["config"][query] = {}
|
||||
module = HomeModel.get_module_by_name(query)
|
||||
mcs = HomeModel.get_module_config_module(module.id)
|
||||
for mc in mcs:
|
||||
config_db = HomeModel.get_config(mc.config_id)
|
||||
request_json["config"][query][config_db.name] = mc.value
|
||||
return request_json["config"]
|
||||
|
||||
def start(self):
|
||||
"""Start all worker"""
|
||||
cp = 0
|
||||
for i in self.query:
|
||||
for j in self.modules_list:
|
||||
self.jobs.put((cp, i, j))
|
||||
cp += 1
|
||||
#need the index and the url in each queue item.
|
||||
for _ in range(self.thread_count):
|
||||
worker = Thread(target=self.process)
|
||||
worker.daemon = True
|
||||
worker.start()
|
||||
self.threads.append(worker)
|
||||
|
||||
def status(self):
|
||||
"""Status of the current queue"""
|
||||
if self.jobs.empty():
|
||||
self.stop()
|
||||
|
||||
total = len(self.modules_list)
|
||||
remaining = max(self.jobs.qsize(), len(self.threads))
|
||||
complete = total - remaining
|
||||
registered = len(self.result)
|
||||
|
||||
return {
|
||||
'id': self.uuid,
|
||||
'total': total,
|
||||
'complete': complete,
|
||||
'remaining': remaining,
|
||||
'registered': registered,
|
||||
'stopped' : self.stopped,
|
||||
"nb_errors": self.nb_errors
|
||||
}
|
||||
|
||||
def stop(self):
|
||||
"""Stop the current queue and worker"""
|
||||
self.jobs.queue.clear()
|
||||
|
||||
for worker in self.threads:
|
||||
worker.join(3.5)
|
||||
|
||||
self.threads.clear()
|
||||
sessions.remove(self)
|
||||
self.save_info()
|
||||
|
||||
def process(self):
|
||||
"""Threaded function for queue processing."""
|
||||
while not self.jobs.empty():
|
||||
work = self.jobs.get()
|
||||
|
||||
modules = query_get_module()
|
||||
loc_query = {}
|
||||
self.result[work[1]] = dict()
|
||||
# If Misp format
|
||||
for module in modules:
|
||||
if module["name"] == work[2]:
|
||||
if "format" in module["mispattributes"]:
|
||||
loc_query = {
|
||||
"type": self.input_query,
|
||||
"value": work[1],
|
||||
"uuid": str(uuid.uuid4())
|
||||
}
|
||||
break
|
||||
|
||||
loc_config = {}
|
||||
if work[2] in self.config_module:
|
||||
loc_config = self.config_module[work[2]]
|
||||
|
||||
if loc_query:
|
||||
send_to = {"module": work[2], "attribute": loc_query, "config": loc_config}
|
||||
else:
|
||||
send_to = {"module": work[2], self.input_query: work[1], "config": loc_config}
|
||||
res = query_post_query(send_to)
|
||||
|
||||
## Sort attr in object by ui-priority
|
||||
if res:
|
||||
if "results" in res:
|
||||
if "Object" in res["results"]:
|
||||
for obj in res["results"]["Object"]:
|
||||
loc_obj = get_object(obj["name"])
|
||||
if loc_obj:
|
||||
for attr in obj["Attribute"]:
|
||||
attr["ui-priority"] = loc_obj["attributes"][attr["object_relation"]]["ui-priority"]
|
||||
|
||||
# After adding 'ui-priority'
|
||||
obj["Attribute"].sort(key=lambda x: x["ui-priority"], reverse=True)
|
||||
|
||||
if res and "error" in res:
|
||||
self.nb_errors += 1
|
||||
self.result[work[1]][work[2]] = res
|
||||
|
||||
self.jobs.task_done()
|
||||
return True
|
||||
|
||||
def get_result(self):
|
||||
return self.result
|
||||
|
||||
def save_info(self):
|
||||
"""Save info in the db"""
|
||||
s = Session_db(
|
||||
uuid=str(self.uuid),
|
||||
modules_list=json.dumps(self.modules_list),
|
||||
query_enter=json.dumps(self.query),
|
||||
input_query=self.input_query,
|
||||
config_module=json.dumps(self.config_module),
|
||||
result=json.dumps(self.result),
|
||||
nb_errors=self.nb_errors,
|
||||
query_date=self.query_date
|
||||
)
|
||||
db.session.add(s)
|
||||
db.session.commit()
|
||||
|
||||
h = History(
|
||||
session_id=s.id
|
||||
)
|
||||
db.session.add(h)
|
||||
db.session.commit()
|
||||
|
||||
histories = History.query.all()
|
||||
|
||||
while len(histories) > get_limit_queries():
|
||||
history = History.query.order_by(History.id).all()
|
||||
session = Session_db.query.filter_by(id=history[0].session_id)
|
||||
if not History_Tree.query.filter_by(session_uuid=session.uuid):
|
||||
Session_db.query.filter_by(id=history[0].session_id).delete()
|
||||
History.query.filter_by(id=history[0].id).delete()
|
||||
|
||||
histories = History.query.all()
|
||||
|
||||
db.session.commit()
|
||||
return
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,591 @@
|
|||
/*!
|
||||
* Bootstrap Reboot v5.3.0-alpha1 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2022 The Bootstrap Authors
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||
*/
|
||||
:root,
|
||||
[data-bs-theme=light] {
|
||||
--bs-blue: #0d6efd;
|
||||
--bs-indigo: #6610f2;
|
||||
--bs-purple: #6f42c1;
|
||||
--bs-pink: #d63384;
|
||||
--bs-red: #dc3545;
|
||||
--bs-orange: #fd7e14;
|
||||
--bs-yellow: #ffc107;
|
||||
--bs-green: #198754;
|
||||
--bs-teal: #20c997;
|
||||
--bs-cyan: #0dcaf0;
|
||||
--bs-black: #000;
|
||||
--bs-white: #fff;
|
||||
--bs-gray: #6c757d;
|
||||
--bs-gray-dark: #343a40;
|
||||
--bs-gray-100: #f8f9fa;
|
||||
--bs-gray-200: #e9ecef;
|
||||
--bs-gray-300: #dee2e6;
|
||||
--bs-gray-400: #ced4da;
|
||||
--bs-gray-500: #adb5bd;
|
||||
--bs-gray-600: #6c757d;
|
||||
--bs-gray-700: #495057;
|
||||
--bs-gray-800: #343a40;
|
||||
--bs-gray-900: #212529;
|
||||
--bs-primary: #0d6efd;
|
||||
--bs-secondary: #6c757d;
|
||||
--bs-success: #198754;
|
||||
--bs-info: #0dcaf0;
|
||||
--bs-warning: #ffc107;
|
||||
--bs-danger: #dc3545;
|
||||
--bs-light: #f8f9fa;
|
||||
--bs-dark: #212529;
|
||||
--bs-primary-rgb: 13, 110, 253;
|
||||
--bs-secondary-rgb: 108, 117, 125;
|
||||
--bs-success-rgb: 25, 135, 84;
|
||||
--bs-info-rgb: 13, 202, 240;
|
||||
--bs-warning-rgb: 255, 193, 7;
|
||||
--bs-danger-rgb: 220, 53, 69;
|
||||
--bs-light-rgb: 248, 249, 250;
|
||||
--bs-dark-rgb: 33, 37, 41;
|
||||
--bs-primary-text: #0a58ca;
|
||||
--bs-secondary-text: #6c757d;
|
||||
--bs-success-text: #146c43;
|
||||
--bs-info-text: #087990;
|
||||
--bs-warning-text: #997404;
|
||||
--bs-danger-text: #b02a37;
|
||||
--bs-light-text: #6c757d;
|
||||
--bs-dark-text: #495057;
|
||||
--bs-primary-bg-subtle: #cfe2ff;
|
||||
--bs-secondary-bg-subtle: #f8f9fa;
|
||||
--bs-success-bg-subtle: #d1e7dd;
|
||||
--bs-info-bg-subtle: #cff4fc;
|
||||
--bs-warning-bg-subtle: #fff3cd;
|
||||
--bs-danger-bg-subtle: #f8d7da;
|
||||
--bs-light-bg-subtle: #fcfcfd;
|
||||
--bs-dark-bg-subtle: #ced4da;
|
||||
--bs-primary-border-subtle: #9ec5fe;
|
||||
--bs-secondary-border-subtle: #e9ecef;
|
||||
--bs-success-border-subtle: #a3cfbb;
|
||||
--bs-info-border-subtle: #9eeaf9;
|
||||
--bs-warning-border-subtle: #ffe69c;
|
||||
--bs-danger-border-subtle: #f1aeb5;
|
||||
--bs-light-border-subtle: #e9ecef;
|
||||
--bs-dark-border-subtle: #adb5bd;
|
||||
--bs-white-rgb: 255, 255, 255;
|
||||
--bs-black-rgb: 0, 0, 0;
|
||||
--bs-body-color-rgb: 33, 37, 41;
|
||||
--bs-body-bg-rgb: 255, 255, 255;
|
||||
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
|
||||
--bs-body-font-family: var(--bs-font-sans-serif);
|
||||
--bs-body-font-size: 1rem;
|
||||
--bs-body-font-weight: 400;
|
||||
--bs-body-line-height: 1.5;
|
||||
--bs-body-color: #212529;
|
||||
--bs-emphasis-color: #000;
|
||||
--bs-emphasis-color-rgb: 0, 0, 0;
|
||||
--bs-secondary-color: rgba(33, 37, 41, 0.75);
|
||||
--bs-secondary-color-rgb: 33, 37, 41;
|
||||
--bs-secondary-bg: #e9ecef;
|
||||
--bs-secondary-bg-rgb: 233, 236, 239;
|
||||
--bs-tertiary-color: rgba(33, 37, 41, 0.5);
|
||||
--bs-tertiary-color-rgb: 33, 37, 41;
|
||||
--bs-tertiary-bg: #f8f9fa;
|
||||
--bs-tertiary-bg-rgb: 248, 249, 250;
|
||||
--bs-body-bg: #fff;
|
||||
--bs-body-bg-rgb: 255, 255, 255;
|
||||
--bs-link-color: #0d6efd;
|
||||
--bs-link-color-rgb: 13, 110, 253;
|
||||
--bs-link-decoration: underline;
|
||||
--bs-link-hover-color: #0a58ca;
|
||||
--bs-link-hover-color-rgb: 10, 88, 202;
|
||||
--bs-code-color: #d63384;
|
||||
--bs-highlight-bg: #fff3cd;
|
||||
--bs-border-width: 1px;
|
||||
--bs-border-style: solid;
|
||||
--bs-border-color: #dee2e6;
|
||||
--bs-border-color-translucent: rgba(0, 0, 0, 0.175);
|
||||
--bs-border-radius: 0.375rem;
|
||||
--bs-border-radius-sm: 0.25rem;
|
||||
--bs-border-radius-lg: 0.5rem;
|
||||
--bs-border-radius-xl: 1rem;
|
||||
--bs-border-radius-2xl: 2rem;
|
||||
--bs-border-radius-pill: 50rem;
|
||||
--bs-box-shadow: 0 0.5rem 1rem rgba(var(--bs-body-color-rgb), 0.15);
|
||||
--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(var(--bs-body-color-rgb), 0.075);
|
||||
--bs-box-shadow-lg: 0 1rem 3rem rgba(var(--bs-body-color-rgb), 0.175);
|
||||
--bs-box-shadow-inset: inset 0 1px 2px rgba(var(--bs-body-color-rgb), 0.075);
|
||||
--bs-emphasis-color: #000;
|
||||
--bs-form-control-bg: var(--bs-body-bg);
|
||||
--bs-form-control-disabled-bg: var(--bs-secondary-bg);
|
||||
--bs-highlight-bg: #fff3cd;
|
||||
--bs-breakpoint-xs: 0;
|
||||
--bs-breakpoint-sm: 576px;
|
||||
--bs-breakpoint-md: 768px;
|
||||
--bs-breakpoint-lg: 992px;
|
||||
--bs-breakpoint-xl: 1200px;
|
||||
--bs-breakpoint-xxl: 1400px;
|
||||
}
|
||||
|
||||
[data-bs-theme=dark] {
|
||||
--bs-body-color: #adb5bd;
|
||||
--bs-body-color-rgb: 173, 181, 189;
|
||||
--bs-body-bg: #212529;
|
||||
--bs-body-bg-rgb: 33, 37, 41;
|
||||
--bs-emphasis-color: #f8f9fa;
|
||||
--bs-emphasis-color-rgb: 248, 249, 250;
|
||||
--bs-secondary-color: rgba(173, 181, 189, 0.75);
|
||||
--bs-secondary-color-rgb: 173, 181, 189;
|
||||
--bs-secondary-bg: #343a40;
|
||||
--bs-secondary-bg-rgb: 52, 58, 64;
|
||||
--bs-tertiary-color: rgba(173, 181, 189, 0.5);
|
||||
--bs-tertiary-color-rgb: 173, 181, 189;
|
||||
--bs-tertiary-bg: #2b3035;
|
||||
--bs-tertiary-bg-rgb: 43, 48, 53;
|
||||
--bs-emphasis-color: #fff;
|
||||
--bs-primary-text: #6ea8fe;
|
||||
--bs-secondary-text: #dee2e6;
|
||||
--bs-success-text: #75b798;
|
||||
--bs-info-text: #6edff6;
|
||||
--bs-warning-text: #ffda6a;
|
||||
--bs-danger-text: #ea868f;
|
||||
--bs-light-text: #f8f9fa;
|
||||
--bs-dark-text: #dee2e6;
|
||||
--bs-primary-bg-subtle: #031633;
|
||||
--bs-secondary-bg-subtle: #212529;
|
||||
--bs-success-bg-subtle: #051b11;
|
||||
--bs-info-bg-subtle: #032830;
|
||||
--bs-warning-bg-subtle: #332701;
|
||||
--bs-danger-bg-subtle: #2c0b0e;
|
||||
--bs-light-bg-subtle: #343a40;
|
||||
--bs-dark-bg-subtle: #1a1d20;
|
||||
--bs-primary-border-subtle: #084298;
|
||||
--bs-secondary-border-subtle: #495057;
|
||||
--bs-success-border-subtle: #0f5132;
|
||||
--bs-info-border-subtle: #055160;
|
||||
--bs-warning-border-subtle: #664d03;
|
||||
--bs-danger-border-subtle: #842029;
|
||||
--bs-light-border-subtle: #495057;
|
||||
--bs-dark-border-subtle: #343a40;
|
||||
--bs-heading-color: #fff;
|
||||
--bs-link-color: #6ea8fe;
|
||||
--bs-link-hover-color: #9ec5fe;
|
||||
--bs-link-color-rgb: 110, 168, 254;
|
||||
--bs-link-hover-color-rgb: 158, 197, 254;
|
||||
--bs-code-color: #e685b5;
|
||||
--bs-border-color: #495057;
|
||||
--bs-border-color-translucent: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
:root {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: var(--bs-body-font-family);
|
||||
font-size: var(--bs-body-font-size);
|
||||
font-weight: var(--bs-body-font-weight);
|
||||
line-height: var(--bs-body-line-height);
|
||||
color: var(--bs-body-color);
|
||||
text-align: var(--bs-body-text-align);
|
||||
background-color: var(--bs-body-bg);
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: 1rem 0;
|
||||
color: inherit;
|
||||
border: 0;
|
||||
border-top: var(--bs-border-width) solid;
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
h6, h5, h4, h3, h2, h1 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
color: var(--bs-heading-color, inherit);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: calc(1.375rem + 1.5vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: calc(1.325rem + 0.9vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h2 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: calc(1.3rem + 0.6vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h3 {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: calc(1.275rem + 0.3vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h4 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
abbr[title] {
|
||||
-webkit-text-decoration: underline dotted;
|
||||
text-decoration: underline dotted;
|
||||
cursor: help;
|
||||
-webkit-text-decoration-skip-ink: none;
|
||||
text-decoration-skip-ink: none;
|
||||
}
|
||||
|
||||
address {
|
||||
margin-bottom: 1rem;
|
||||
font-style: normal;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul {
|
||||
padding-left: 2rem;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul,
|
||||
dl {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
ol ol,
|
||||
ul ul,
|
||||
ol ul,
|
||||
ul ol {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
dt {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin-bottom: 0.5rem;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
small {
|
||||
font-size: 0.875em;
|
||||
}
|
||||
|
||||
mark {
|
||||
padding: 0.1875em;
|
||||
background-color: var(--bs-highlight-bg);
|
||||
}
|
||||
|
||||
sub,
|
||||
sup {
|
||||
position: relative;
|
||||
font-size: 0.75em;
|
||||
line-height: 0;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
a {
|
||||
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));
|
||||
text-decoration: underline;
|
||||
}
|
||||
a:hover {
|
||||
--bs-link-color-rgb: var(--bs-link-hover-color-rgb);
|
||||
}
|
||||
|
||||
a:not([href]):not([class]), a:not([href]):not([class]):hover {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
pre,
|
||||
code,
|
||||
kbd,
|
||||
samp {
|
||||
font-family: var(--bs-font-monospace);
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
pre {
|
||||
display: block;
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
overflow: auto;
|
||||
font-size: 0.875em;
|
||||
}
|
||||
pre code {
|
||||
font-size: inherit;
|
||||
color: inherit;
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
code {
|
||||
font-size: 0.875em;
|
||||
color: var(--bs-code-color);
|
||||
word-wrap: break-word;
|
||||
}
|
||||
a > code {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
kbd {
|
||||
padding: 0.1875rem 0.375rem;
|
||||
font-size: 0.875em;
|
||||
color: var(--bs-body-bg);
|
||||
background-color: var(--bs-body-color);
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
kbd kbd {
|
||||
padding: 0;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
figure {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
img,
|
||||
svg {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
table {
|
||||
caption-side: bottom;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
caption {
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
color: var(--bs-secondary-color);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: inherit;
|
||||
text-align: -webkit-match-parent;
|
||||
}
|
||||
|
||||
thead,
|
||||
tbody,
|
||||
tfoot,
|
||||
tr,
|
||||
td,
|
||||
th {
|
||||
border-color: inherit;
|
||||
border-style: solid;
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
button:focus:not(:focus-visible) {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
input,
|
||||
button,
|
||||
select,
|
||||
optgroup,
|
||||
textarea {
|
||||
margin: 0;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
button,
|
||||
select {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
[role=button] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
select {
|
||||
word-wrap: normal;
|
||||
}
|
||||
select:disabled {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
button,
|
||||
[type=button],
|
||||
[type=reset],
|
||||
[type=submit] {
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
button:not(:disabled),
|
||||
[type=button]:not(:disabled),
|
||||
[type=reset]:not(:disabled),
|
||||
[type=submit]:not(:disabled) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
::-moz-focus-inner {
|
||||
padding: 0;
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
legend {
|
||||
float: left;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: calc(1.275rem + 0.3vw);
|
||||
line-height: inherit;
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
legend {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
legend + * {
|
||||
clear: left;
|
||||
}
|
||||
|
||||
::-webkit-datetime-edit-fields-wrapper,
|
||||
::-webkit-datetime-edit-text,
|
||||
::-webkit-datetime-edit-minute,
|
||||
::-webkit-datetime-edit-hour-field,
|
||||
::-webkit-datetime-edit-day-field,
|
||||
::-webkit-datetime-edit-month-field,
|
||||
::-webkit-datetime-edit-year-field {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::-webkit-inner-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
[type=search] {
|
||||
outline-offset: -2px;
|
||||
-webkit-appearance: textfield;
|
||||
}
|
||||
|
||||
/* rtl:raw:
|
||||
[type="tel"],
|
||||
[type="url"],
|
||||
[type="email"],
|
||||
[type="number"] {
|
||||
direction: ltr;
|
||||
}
|
||||
*/
|
||||
::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
::-webkit-color-swatch-wrapper {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
font: inherit;
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
::file-selector-button {
|
||||
font: inherit;
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
output {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
iframe {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/*# sourceMappingURL=bootstrap-reboot.css.map */
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,588 @@
|
|||
/*!
|
||||
* Bootstrap Reboot v5.3.0-alpha1 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2022 The Bootstrap Authors
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||
*/
|
||||
:root,
|
||||
[data-bs-theme=light] {
|
||||
--bs-blue: #0d6efd;
|
||||
--bs-indigo: #6610f2;
|
||||
--bs-purple: #6f42c1;
|
||||
--bs-pink: #d63384;
|
||||
--bs-red: #dc3545;
|
||||
--bs-orange: #fd7e14;
|
||||
--bs-yellow: #ffc107;
|
||||
--bs-green: #198754;
|
||||
--bs-teal: #20c997;
|
||||
--bs-cyan: #0dcaf0;
|
||||
--bs-black: #000;
|
||||
--bs-white: #fff;
|
||||
--bs-gray: #6c757d;
|
||||
--bs-gray-dark: #343a40;
|
||||
--bs-gray-100: #f8f9fa;
|
||||
--bs-gray-200: #e9ecef;
|
||||
--bs-gray-300: #dee2e6;
|
||||
--bs-gray-400: #ced4da;
|
||||
--bs-gray-500: #adb5bd;
|
||||
--bs-gray-600: #6c757d;
|
||||
--bs-gray-700: #495057;
|
||||
--bs-gray-800: #343a40;
|
||||
--bs-gray-900: #212529;
|
||||
--bs-primary: #0d6efd;
|
||||
--bs-secondary: #6c757d;
|
||||
--bs-success: #198754;
|
||||
--bs-info: #0dcaf0;
|
||||
--bs-warning: #ffc107;
|
||||
--bs-danger: #dc3545;
|
||||
--bs-light: #f8f9fa;
|
||||
--bs-dark: #212529;
|
||||
--bs-primary-rgb: 13, 110, 253;
|
||||
--bs-secondary-rgb: 108, 117, 125;
|
||||
--bs-success-rgb: 25, 135, 84;
|
||||
--bs-info-rgb: 13, 202, 240;
|
||||
--bs-warning-rgb: 255, 193, 7;
|
||||
--bs-danger-rgb: 220, 53, 69;
|
||||
--bs-light-rgb: 248, 249, 250;
|
||||
--bs-dark-rgb: 33, 37, 41;
|
||||
--bs-primary-text: #0a58ca;
|
||||
--bs-secondary-text: #6c757d;
|
||||
--bs-success-text: #146c43;
|
||||
--bs-info-text: #087990;
|
||||
--bs-warning-text: #997404;
|
||||
--bs-danger-text: #b02a37;
|
||||
--bs-light-text: #6c757d;
|
||||
--bs-dark-text: #495057;
|
||||
--bs-primary-bg-subtle: #cfe2ff;
|
||||
--bs-secondary-bg-subtle: #f8f9fa;
|
||||
--bs-success-bg-subtle: #d1e7dd;
|
||||
--bs-info-bg-subtle: #cff4fc;
|
||||
--bs-warning-bg-subtle: #fff3cd;
|
||||
--bs-danger-bg-subtle: #f8d7da;
|
||||
--bs-light-bg-subtle: #fcfcfd;
|
||||
--bs-dark-bg-subtle: #ced4da;
|
||||
--bs-primary-border-subtle: #9ec5fe;
|
||||
--bs-secondary-border-subtle: #e9ecef;
|
||||
--bs-success-border-subtle: #a3cfbb;
|
||||
--bs-info-border-subtle: #9eeaf9;
|
||||
--bs-warning-border-subtle: #ffe69c;
|
||||
--bs-danger-border-subtle: #f1aeb5;
|
||||
--bs-light-border-subtle: #e9ecef;
|
||||
--bs-dark-border-subtle: #adb5bd;
|
||||
--bs-white-rgb: 255, 255, 255;
|
||||
--bs-black-rgb: 0, 0, 0;
|
||||
--bs-body-color-rgb: 33, 37, 41;
|
||||
--bs-body-bg-rgb: 255, 255, 255;
|
||||
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
|
||||
--bs-body-font-family: var(--bs-font-sans-serif);
|
||||
--bs-body-font-size: 1rem;
|
||||
--bs-body-font-weight: 400;
|
||||
--bs-body-line-height: 1.5;
|
||||
--bs-body-color: #212529;
|
||||
--bs-emphasis-color: #000;
|
||||
--bs-emphasis-color-rgb: 0, 0, 0;
|
||||
--bs-secondary-color: rgba(33, 37, 41, 0.75);
|
||||
--bs-secondary-color-rgb: 33, 37, 41;
|
||||
--bs-secondary-bg: #e9ecef;
|
||||
--bs-secondary-bg-rgb: 233, 236, 239;
|
||||
--bs-tertiary-color: rgba(33, 37, 41, 0.5);
|
||||
--bs-tertiary-color-rgb: 33, 37, 41;
|
||||
--bs-tertiary-bg: #f8f9fa;
|
||||
--bs-tertiary-bg-rgb: 248, 249, 250;
|
||||
--bs-body-bg: #fff;
|
||||
--bs-body-bg-rgb: 255, 255, 255;
|
||||
--bs-link-color: #0d6efd;
|
||||
--bs-link-color-rgb: 13, 110, 253;
|
||||
--bs-link-decoration: underline;
|
||||
--bs-link-hover-color: #0a58ca;
|
||||
--bs-link-hover-color-rgb: 10, 88, 202;
|
||||
--bs-code-color: #d63384;
|
||||
--bs-highlight-bg: #fff3cd;
|
||||
--bs-border-width: 1px;
|
||||
--bs-border-style: solid;
|
||||
--bs-border-color: #dee2e6;
|
||||
--bs-border-color-translucent: rgba(0, 0, 0, 0.175);
|
||||
--bs-border-radius: 0.375rem;
|
||||
--bs-border-radius-sm: 0.25rem;
|
||||
--bs-border-radius-lg: 0.5rem;
|
||||
--bs-border-radius-xl: 1rem;
|
||||
--bs-border-radius-2xl: 2rem;
|
||||
--bs-border-radius-pill: 50rem;
|
||||
--bs-box-shadow: 0 0.5rem 1rem rgba(var(--bs-body-color-rgb), 0.15);
|
||||
--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(var(--bs-body-color-rgb), 0.075);
|
||||
--bs-box-shadow-lg: 0 1rem 3rem rgba(var(--bs-body-color-rgb), 0.175);
|
||||
--bs-box-shadow-inset: inset 0 1px 2px rgba(var(--bs-body-color-rgb), 0.075);
|
||||
--bs-emphasis-color: #000;
|
||||
--bs-form-control-bg: var(--bs-body-bg);
|
||||
--bs-form-control-disabled-bg: var(--bs-secondary-bg);
|
||||
--bs-highlight-bg: #fff3cd;
|
||||
--bs-breakpoint-xs: 0;
|
||||
--bs-breakpoint-sm: 576px;
|
||||
--bs-breakpoint-md: 768px;
|
||||
--bs-breakpoint-lg: 992px;
|
||||
--bs-breakpoint-xl: 1200px;
|
||||
--bs-breakpoint-xxl: 1400px;
|
||||
}
|
||||
|
||||
[data-bs-theme=dark] {
|
||||
--bs-body-color: #adb5bd;
|
||||
--bs-body-color-rgb: 173, 181, 189;
|
||||
--bs-body-bg: #212529;
|
||||
--bs-body-bg-rgb: 33, 37, 41;
|
||||
--bs-emphasis-color: #f8f9fa;
|
||||
--bs-emphasis-color-rgb: 248, 249, 250;
|
||||
--bs-secondary-color: rgba(173, 181, 189, 0.75);
|
||||
--bs-secondary-color-rgb: 173, 181, 189;
|
||||
--bs-secondary-bg: #343a40;
|
||||
--bs-secondary-bg-rgb: 52, 58, 64;
|
||||
--bs-tertiary-color: rgba(173, 181, 189, 0.5);
|
||||
--bs-tertiary-color-rgb: 173, 181, 189;
|
||||
--bs-tertiary-bg: #2b3035;
|
||||
--bs-tertiary-bg-rgb: 43, 48, 53;
|
||||
--bs-emphasis-color: #fff;
|
||||
--bs-primary-text: #6ea8fe;
|
||||
--bs-secondary-text: #dee2e6;
|
||||
--bs-success-text: #75b798;
|
||||
--bs-info-text: #6edff6;
|
||||
--bs-warning-text: #ffda6a;
|
||||
--bs-danger-text: #ea868f;
|
||||
--bs-light-text: #f8f9fa;
|
||||
--bs-dark-text: #dee2e6;
|
||||
--bs-primary-bg-subtle: #031633;
|
||||
--bs-secondary-bg-subtle: #212529;
|
||||
--bs-success-bg-subtle: #051b11;
|
||||
--bs-info-bg-subtle: #032830;
|
||||
--bs-warning-bg-subtle: #332701;
|
||||
--bs-danger-bg-subtle: #2c0b0e;
|
||||
--bs-light-bg-subtle: #343a40;
|
||||
--bs-dark-bg-subtle: #1a1d20;
|
||||
--bs-primary-border-subtle: #084298;
|
||||
--bs-secondary-border-subtle: #495057;
|
||||
--bs-success-border-subtle: #0f5132;
|
||||
--bs-info-border-subtle: #055160;
|
||||
--bs-warning-border-subtle: #664d03;
|
||||
--bs-danger-border-subtle: #842029;
|
||||
--bs-light-border-subtle: #495057;
|
||||
--bs-dark-border-subtle: #343a40;
|
||||
--bs-heading-color: #fff;
|
||||
--bs-link-color: #6ea8fe;
|
||||
--bs-link-hover-color: #9ec5fe;
|
||||
--bs-link-color-rgb: 110, 168, 254;
|
||||
--bs-link-hover-color-rgb: 158, 197, 254;
|
||||
--bs-code-color: #e685b5;
|
||||
--bs-border-color: #495057;
|
||||
--bs-border-color-translucent: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
:root {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: var(--bs-body-font-family);
|
||||
font-size: var(--bs-body-font-size);
|
||||
font-weight: var(--bs-body-font-weight);
|
||||
line-height: var(--bs-body-line-height);
|
||||
color: var(--bs-body-color);
|
||||
text-align: var(--bs-body-text-align);
|
||||
background-color: var(--bs-body-bg);
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: 1rem 0;
|
||||
color: inherit;
|
||||
border: 0;
|
||||
border-top: var(--bs-border-width) solid;
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
h6, h5, h4, h3, h2, h1 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
color: var(--bs-heading-color, inherit);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: calc(1.375rem + 1.5vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: calc(1.325rem + 0.9vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h2 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: calc(1.3rem + 0.6vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h3 {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: calc(1.275rem + 0.3vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h4 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
abbr[title] {
|
||||
-webkit-text-decoration: underline dotted;
|
||||
text-decoration: underline dotted;
|
||||
cursor: help;
|
||||
-webkit-text-decoration-skip-ink: none;
|
||||
text-decoration-skip-ink: none;
|
||||
}
|
||||
|
||||
address {
|
||||
margin-bottom: 1rem;
|
||||
font-style: normal;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul {
|
||||
padding-right: 2rem;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul,
|
||||
dl {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
ol ol,
|
||||
ul ul,
|
||||
ol ul,
|
||||
ul ol {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
dt {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin-bottom: 0.5rem;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
small {
|
||||
font-size: 0.875em;
|
||||
}
|
||||
|
||||
mark {
|
||||
padding: 0.1875em;
|
||||
background-color: var(--bs-highlight-bg);
|
||||
}
|
||||
|
||||
sub,
|
||||
sup {
|
||||
position: relative;
|
||||
font-size: 0.75em;
|
||||
line-height: 0;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
a {
|
||||
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));
|
||||
text-decoration: underline;
|
||||
}
|
||||
a:hover {
|
||||
--bs-link-color-rgb: var(--bs-link-hover-color-rgb);
|
||||
}
|
||||
|
||||
a:not([href]):not([class]), a:not([href]):not([class]):hover {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
pre,
|
||||
code,
|
||||
kbd,
|
||||
samp {
|
||||
font-family: var(--bs-font-monospace);
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
pre {
|
||||
display: block;
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
overflow: auto;
|
||||
font-size: 0.875em;
|
||||
}
|
||||
pre code {
|
||||
font-size: inherit;
|
||||
color: inherit;
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
code {
|
||||
font-size: 0.875em;
|
||||
color: var(--bs-code-color);
|
||||
word-wrap: break-word;
|
||||
}
|
||||
a > code {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
kbd {
|
||||
padding: 0.1875rem 0.375rem;
|
||||
font-size: 0.875em;
|
||||
color: var(--bs-body-bg);
|
||||
background-color: var(--bs-body-color);
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
kbd kbd {
|
||||
padding: 0;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
figure {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
img,
|
||||
svg {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
table {
|
||||
caption-side: bottom;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
caption {
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
color: var(--bs-secondary-color);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: inherit;
|
||||
text-align: -webkit-match-parent;
|
||||
}
|
||||
|
||||
thead,
|
||||
tbody,
|
||||
tfoot,
|
||||
tr,
|
||||
td,
|
||||
th {
|
||||
border-color: inherit;
|
||||
border-style: solid;
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
button:focus:not(:focus-visible) {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
input,
|
||||
button,
|
||||
select,
|
||||
optgroup,
|
||||
textarea {
|
||||
margin: 0;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
button,
|
||||
select {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
[role=button] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
select {
|
||||
word-wrap: normal;
|
||||
}
|
||||
select:disabled {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
button,
|
||||
[type=button],
|
||||
[type=reset],
|
||||
[type=submit] {
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
button:not(:disabled),
|
||||
[type=button]:not(:disabled),
|
||||
[type=reset]:not(:disabled),
|
||||
[type=submit]:not(:disabled) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
::-moz-focus-inner {
|
||||
padding: 0;
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
legend {
|
||||
float: right;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: calc(1.275rem + 0.3vw);
|
||||
line-height: inherit;
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
legend {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
legend + * {
|
||||
clear: right;
|
||||
}
|
||||
|
||||
::-webkit-datetime-edit-fields-wrapper,
|
||||
::-webkit-datetime-edit-text,
|
||||
::-webkit-datetime-edit-minute,
|
||||
::-webkit-datetime-edit-hour-field,
|
||||
::-webkit-datetime-edit-day-field,
|
||||
::-webkit-datetime-edit-month-field,
|
||||
::-webkit-datetime-edit-year-field {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::-webkit-inner-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
[type=search] {
|
||||
outline-offset: -2px;
|
||||
-webkit-appearance: textfield;
|
||||
}
|
||||
|
||||
[type="tel"],
|
||||
[type="url"],
|
||||
[type="email"],
|
||||
[type="number"] {
|
||||
direction: ltr;
|
||||
}
|
||||
::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
::-webkit-color-swatch-wrapper {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
font: inherit;
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
::file-selector-button {
|
||||
font: inherit;
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
output {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
iframe {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
/*# sourceMappingURL=bootstrap-reboot.rtl.css.map */
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,182 @@
|
|||
:root {
|
||||
--sidebar-width: 210px;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #fbfbfb;
|
||||
}
|
||||
|
||||
main
|
||||
{
|
||||
overflow-x: hidden;
|
||||
}
|
||||
span#goTop{
|
||||
position: fixed;
|
||||
right: 1em;
|
||||
bottom: 1em;
|
||||
background-color: #fafbfc;
|
||||
}
|
||||
|
||||
|
||||
.select2-container {
|
||||
padding: 5px
|
||||
}
|
||||
|
||||
.select2-selection__choice{
|
||||
background-color: #ced4da;
|
||||
filter: drop-shadow(-1px 1px 1px rgba(50, 50, 0, 0.5));
|
||||
}
|
||||
|
||||
|
||||
div#searchbox {
|
||||
display: flex;
|
||||
margin: 0 auto 0 auto;
|
||||
}
|
||||
#process-query {
|
||||
font-size: 1.1em;
|
||||
height: 2.8em;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 5px 0 0 5px;
|
||||
padding-left: .75em;
|
||||
outline: none;
|
||||
}
|
||||
button#query {
|
||||
color: white;
|
||||
background-color: #0d6efd;
|
||||
border: 0;
|
||||
border-radius: 0 5px 5px 0;
|
||||
height: 2.8em;
|
||||
font-size: 1.1em;
|
||||
font-weight: 600;
|
||||
flex-grow: 1;
|
||||
outline: none;
|
||||
}
|
||||
button#query:hover {
|
||||
background-color: #2779bd;
|
||||
}
|
||||
|
||||
progress {
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
height: 5px;
|
||||
width: 100%;
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
margin: 5px 0;
|
||||
}
|
||||
progress::-webkit-progress-bar {
|
||||
background-color: transparent;
|
||||
}
|
||||
progress::-webkit-progress-value {
|
||||
background-color: #3490dc;
|
||||
}
|
||||
progress::-moz-progress-bar {
|
||||
background-color: #3490dc;
|
||||
}
|
||||
span#status {
|
||||
float: right;
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
.checkbox-type-module{
|
||||
margin-left: 5px
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*JSON Parser*/
|
||||
#core-format-picker {
|
||||
padding: 5px;
|
||||
border-radius: 5px;
|
||||
background-color: #f9f9f9;
|
||||
border: 1px solid #e1e1e1;
|
||||
}
|
||||
#core-format-picker .selectable-key {
|
||||
font-weight: bold;
|
||||
padding: 2px 3px;
|
||||
}
|
||||
#core-format-picker .selectable-key:hover {
|
||||
box-shadow: 0 0 5px #00000077;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#core-format-picker .selectable-value {
|
||||
padding: 2px 3px;
|
||||
}
|
||||
#core-format-picker .selectable-value:hover {
|
||||
box-shadow: 0 0 5px #00000077;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#core-format-picker .children-counter {
|
||||
background-color: #6b6b6b;
|
||||
color: #ffffff;
|
||||
padding: 2px 3px;
|
||||
margin: 0px 0.25rem;
|
||||
font-size: smaller;
|
||||
border-radius: 3px;
|
||||
}
|
||||
#core-format-picker .collaspe-button {
|
||||
cursor: pointer;
|
||||
}
|
||||
#core-format-picker .collaspe-button:hover {
|
||||
color: #292929;
|
||||
cursor: pointer;
|
||||
}
|
||||
/**/
|
||||
|
||||
|
||||
|
||||
.round-button {
|
||||
width: 3%;
|
||||
}
|
||||
.round-button-circle {
|
||||
padding-bottom: 100%;
|
||||
border-radius: 50%;
|
||||
|
||||
background: #e13333;
|
||||
box-shadow: 0 0 3px gray;
|
||||
}
|
||||
.round-button-circle:hover {
|
||||
background:#c41313;
|
||||
cursor: pointer;
|
||||
}
|
||||
.round-button a {
|
||||
float:left;
|
||||
width:100%;
|
||||
padding-top:50%;
|
||||
padding-bottom:50%;
|
||||
line-height:1em;
|
||||
margin-top:-0.5em;
|
||||
text-align:center;
|
||||
color:#e2eaf3;
|
||||
|
||||
text-decoration:none;
|
||||
}
|
||||
|
||||
.side-panel-config {
|
||||
background-color: white;
|
||||
box-shadow: rgba(0, 0, 0, 0.05) 0px 2px 5px 0px, rgba(0, 0, 0, 0.05) 0px 2px 10px 0px;
|
||||
border-radius: 10px;
|
||||
height: 75vh;
|
||||
position: fixed;
|
||||
max-width: calc((100%) / 2 - 1*var(--bs-gutter-x) * .5);
|
||||
right: calc(var(--bs-gutter-x) * .5);
|
||||
}
|
||||
|
||||
#sidebar-nav {
|
||||
width: var(--sidebar-width);
|
||||
}
|
||||
#sidebar .sidebar-menu-wrapper {
|
||||
position: fixed;
|
||||
}
|
||||
#sidebar.collapsing .sidebar-menu-wrapper {
|
||||
position: relative !important;
|
||||
}
|
||||
|
||||
@media (min-width: 992px) {
|
||||
.side-panel-config {
|
||||
max-width: calc((100% - 200px) / 2 - 1*var(--bs-gutter-x) * .5);
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,2 @@
|
|||
.json-view-item:not(.root-item){margin-left:15px}.value-key{color:var(--jtv-valueKey-color);font-weight:600;margin-left:10px;border-radius:2px;white-space:nowrap;padding:5px 5px 5px 10px}.value-key.can-select{cursor:pointer}.value-key.can-select:hover{background-color:#00000014}.value-key.can-select:focus{outline:2px solid var(--jtv-hover-color)}.data-key{font-size:100%;font-family:inherit;border:0;background-color:transparent;width:100%;color:var(--jtv-key-color);display:flex;align-items:center;border-radius:2px;font-weight:600;cursor:pointer;white-space:nowrap;padding:5px}.data-key:hover{background-color:var(--jtv-hover-color)}.data-key:focus{outline:2px solid var(--jtv-hover-color)}.data-key::-moz-focus-inner{border:0}.data-key .properties{font-weight:300;opacity:.9;margin-left:4px;-webkit-user-select:none;user-select:none}.chevron-arrow{flex-shrink:0;border-right:2px solid var(--jtv-arrow-color);border-bottom:2px solid var(--jtv-arrow-color);width:var(--jtv-arrow-size);height:var(--jtv-arrow-size);margin-right:20px;margin-left:5px;transform:rotate(-45deg)}.chevron-arrow.opened{margin-top:-3px;transform:rotate(45deg)}.root-item[data-v-1cbd6770]{--jtv-key-color: #0977e6;--jtv-valueKey-color: #073642;--jtv-string-color: #268bd2;--jtv-number-color: #2aa198;--jtv-boolean-color: #cb4b16;--jtv-null-color: #6c71c4;--jtv-arrow-size: 6px;--jtv-arrow-color: #444;--jtv-hover-color: rgba(0, 0, 0, .1);margin-left:0;width:100%;height:auto}.root-item.dark[data-v-1cbd6770]{--jtv-key-color: #80d8ff;--jtv-valueKey-color: #fdf6e3;--jtv-hover-color: rgba(255, 255, 255, .1);--jtv-arrow-color: #fdf6e3}
|
||||
|
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue