Merge branch 'main' of github.com:MISP/misp-modules

pull/670/head
Christian Studer 2024-06-29 08:29:59 +02:00
commit ea22f7bd9d
No known key found for this signature in database
GPG Key ID: 6BBED1B63A6D639F
2230 changed files with 143286 additions and 35 deletions

4
.gitignore vendored
View File

@ -19,4 +19,6 @@ site*
venv*
#vscode
.vscode*
.vscode*
*.sqlite
website/conf/config.cfg

View File

@ -76,6 +76,7 @@ ndjson = "0.3.1"
Jinja2 = "3.1.2"
mattermostdriver = "7.3.2"
openpyxl = "*"
slack-sdk = "3.27.1"
[requires]
python_version = "3.7"

View File

@ -3,10 +3,12 @@
[![Python package](https://github.com/MISP/misp-modules/actions/workflows/python-package.yml/badge.svg)](https://github.com/MISP/misp-modules/actions/workflows/python-package.yml)[![Coverage Status](https://coveralls.io/repos/github/MISP/misp-modules/badge.svg?branch=main)](https://coveralls.io/github/MISP/misp-modules?branch=main)
[![codecov](https://codecov.io/gh/MISP/misp-modules/branch/main/graph/badge.svg)](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/).
@ -576,7 +581,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

View File

@ -1,9 +1,8 @@
-i https://pypi.org/simple
aiohttp==3.8.4
aiohttp>=3.9.0
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
apiosintDS==2.0.3
appdirs==1.4.4
argcomplete==3.0.8 ; python_version >= '3.6'
argparse==1.4.0
@ -18,7 +17,7 @@ beautifulsoup4==4.12.2
bidict==0.22.1 ; python_version >= '3.7'
blockchain==1.4.4
censys==2.2.2
certifi==2023.5.7 ; python_version >= '3.6'
certifi>=2023.7.22 ; python_version >= '3.6'
cffi==1.15.1
chardet==5.1.0
charset-normalizer==3.1.0 ; python_full_version >= '3.7.0'
@ -30,7 +29,7 @@ colorclass==2.2.2 ; python_version >= '2.6'
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'
cryptography>=41.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'
@ -96,7 +95,7 @@ pandas-ods-reader==0.1.4
passivetotal==2.5.9
pcodedmp==1.2.6
pdftotext==2.2.2
pillow==9.5.0
pillow>=10.2.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'
@ -104,8 +103,8 @@ 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'
pycryptodome==3.19.1 ; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
pycryptodomex==3.19.1 ; 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
@ -152,6 +151,7 @@ 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'
slack-sdk==3.27.1
sniffio==1.3.0 ; python_version >= '3.7'
socialscan==1.4
socketio-client==0.5.7.4
@ -173,7 +173,6 @@ 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'
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
vt-graph-api==2.2.0
vt-py==0.17.5
vulners==2.0.10

View File

@ -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?
@ -75,7 +76,6 @@ For more information: [Extending MISP with Python modules](https://www.circl.lu/
* [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

BIN
docs/logos/stairwell.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

BIN
docs/logos/whoisfreaks.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View File

@ -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>
@ -1904,23 +1944,24 @@ 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)
#### [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/
> - https://vysion.ai/
> - https://developers.vysion.ai/
> - https://github.com/ByronLabs/vysion-cti/tree/main
- **requirements**:
> Vysion python library
> Vysion API Key
> - Vysion python library
> - Vysion API Key
-----

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

View File

@ -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>
@ -1901,6 +1941,27 @@ 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.
- **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
- **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).

View File

@ -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 bening) 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)"
}

View File

@ -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."
}

View File

@ -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

View File

@ -1 +1 @@
__all__ = ['testaction', 'mattermost']
__all__ = ['testaction', 'mattermost', 'slack']

View File

@ -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

View File

@ -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

View File

@ -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', 'vysion', 'stairwell',
'google_threat_intelligence']
minimum_required_fields = ('type', 'uuid', 'value')

View File

@ -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'],

View File

@ -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())

View File

@ -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()

View File

@ -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

View File

@ -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}')

54
website/README.md Normal file
View File

@ -0,0 +1,54 @@
# MISP-module website
Use all modules with a dedicate website without any MISP
![home](https://github.com/MISP/misp-modules/blob/main/website/doc/home_misp_module.png?raw=true)
![query](https://github.com/MISP/misp-modules/blob/main/website/doc/query_misp_module.png?raw=true)
## 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`.

52
website/app.py Normal file
View File

@ -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"))

45
website/app/__init__.py Normal file
View File

@ -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

View File

@ -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'))

View File

@ -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')

120
website/app/db_class/db.py Normal file
View File

@ -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))

View File

@ -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

View File

@ -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

View File

@ -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')

View File

@ -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"}

View File

@ -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

257
website/app/home.py Normal file
View File

@ -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

235
website/app/home_core.py Normal file
View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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);
}
}

1314
website/app/static/css/jquery-ui.css vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@ -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

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,165 @@
Fonticons, Inc. (https://fontawesome.com)
--------------------------------------------------------------------------------
Font Awesome Free License
Font Awesome Free is free, open source, and GPL friendly. You can use it for
commercial projects, open source projects, or really almost whatever you want.
Full Font Awesome Free license: https://fontawesome.com/license/free.
--------------------------------------------------------------------------------
# Icons: CC BY 4.0 License (https://creativecommons.org/licenses/by/4.0/)
The Font Awesome Free download is licensed under a Creative Commons
Attribution 4.0 International License and applies to all icons packaged
as SVG and JS file types.
--------------------------------------------------------------------------------
# Fonts: SIL OFL 1.1 License
In the Font Awesome Free download, the SIL OFL license applies to all icons
packaged as web and desktop font files.
Copyright (c) 2023 Fonticons, Inc. (https://fontawesome.com)
with Reserved Font Name: "Font Awesome".
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
SIL OPEN FONT LICENSE
Version 1.1 - 26 February 2007
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting — in part or in whole — any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
--------------------------------------------------------------------------------
# Code: MIT License (https://opensource.org/licenses/MIT)
In the Font Awesome Free download, the MIT license applies to all non-font and
non-icon files.
Copyright 2023 Fonticons, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in the
Software without restriction, including without limitation the rights to use, copy,
modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
and to permit persons to whom the Software is furnished to do so, subject to the
following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
# Attribution
Attribution is required by MIT, SIL OFL, and CC BY licenses. Downloaded Font
Awesome Free files already contain embedded comments with sufficient
attribution, so you shouldn't need to do anything additional when using these
files normally.
We've kept attribution comments terse, so we ask that you do not actively work
to remove them from files, especially code. They're a great way for folks to
learn about Font Awesome.
--------------------------------------------------------------------------------
# Brand Icons
All brand icons are trademarks of their respective owners. The use of these
trademarks does not indicate endorsement of the trademark holder by Font
Awesome, nor vice versa. **Please do not use brand logos for any purpose except
to represent the company, product, or service to which they refer.**

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 it is too large Load Diff

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

View File

@ -0,0 +1,19 @@
/*!
* Font Awesome Free 6.3.0 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
* Copyright 2023 Fonticons, Inc.
*/
:root, :host {
--fa-style-family-classic: 'Font Awesome 6 Free';
--fa-font-regular: normal 400 1em/1 'Font Awesome 6 Free'; }
@font-face {
font-family: 'Font Awesome 6 Free';
font-style: normal;
font-weight: 400;
font-display: block;
src: url("../webfonts/fa-regular-400.woff2") format("woff2"), url("../webfonts/fa-regular-400.ttf") format("truetype"); }
.far,
.fa-regular {
font-weight: 400; }

View File

@ -0,0 +1,6 @@
/*!
* Font Awesome Free 6.3.0 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
* Copyright 2023 Fonticons, Inc.
*/
:host,:root{--fa-style-family-classic:"Font Awesome 6 Free";--fa-font-regular:normal 400 1em/1 "Font Awesome 6 Free"}@font-face{font-family:"Font Awesome 6 Free";font-style:normal;font-weight:400;font-display:block;src:url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.ttf) format("truetype")}.fa-regular,.far{font-weight:400}

View File

@ -0,0 +1,19 @@
/*!
* Font Awesome Free 6.3.0 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
* Copyright 2023 Fonticons, Inc.
*/
:root, :host {
--fa-style-family-classic: 'Font Awesome 6 Free';
--fa-font-solid: normal 900 1em/1 'Font Awesome 6 Free'; }
@font-face {
font-family: 'Font Awesome 6 Free';
font-style: normal;
font-weight: 900;
font-display: block;
src: url("../webfonts/fa-solid-900.woff2") format("woff2"), url("../webfonts/fa-solid-900.ttf") format("truetype"); }
.fas,
.fa-solid {
font-weight: 900; }

View File

@ -0,0 +1,6 @@
/*!
* Font Awesome Free 6.3.0 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
* Copyright 2023 Fonticons, Inc.
*/
:host,:root{--fa-style-family-classic:"Font Awesome 6 Free";--fa-font-solid:normal 900 1em/1 "Font Awesome 6 Free"}@font-face{font-family:"Font Awesome 6 Free";font-style:normal;font-weight:900;font-display:block;src:url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.ttf) format("truetype")}.fa-solid,.fas{font-weight:900}

View File

@ -0,0 +1,638 @@
/*!
* Font Awesome Free 6.3.0 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
* Copyright 2023 Fonticons, Inc.
*/
:root, :host {
--fa-font-solid: normal 900 1em/1 'Font Awesome 6 Solid';
--fa-font-regular: normal 400 1em/1 'Font Awesome 6 Regular';
--fa-font-light: normal 300 1em/1 'Font Awesome 6 Light';
--fa-font-thin: normal 100 1em/1 'Font Awesome 6 Thin';
--fa-font-duotone: normal 900 1em/1 'Font Awesome 6 Duotone';
--fa-font-sharp-solid: normal 900 1em/1 'Font Awesome 6 Sharp';
--fa-font-sharp-regular: normal 400 1em/1 'Font Awesome 6 Sharp';
--fa-font-brands: normal 400 1em/1 'Font Awesome 6 Brands'; }
svg:not(:root).svg-inline--fa, svg:not(:host).svg-inline--fa {
overflow: visible;
box-sizing: content-box; }
.svg-inline--fa {
display: var(--fa-display, inline-block);
height: 1em;
overflow: visible;
vertical-align: -.125em; }
.svg-inline--fa.fa-2xs {
vertical-align: 0.1em; }
.svg-inline--fa.fa-xs {
vertical-align: 0em; }
.svg-inline--fa.fa-sm {
vertical-align: -0.07143em; }
.svg-inline--fa.fa-lg {
vertical-align: -0.2em; }
.svg-inline--fa.fa-xl {
vertical-align: -0.25em; }
.svg-inline--fa.fa-2xl {
vertical-align: -0.3125em; }
.svg-inline--fa.fa-pull-left {
margin-right: var(--fa-pull-margin, 0.3em);
width: auto; }
.svg-inline--fa.fa-pull-right {
margin-left: var(--fa-pull-margin, 0.3em);
width: auto; }
.svg-inline--fa.fa-li {
width: var(--fa-li-width, 2em);
top: 0.25em; }
.svg-inline--fa.fa-fw {
width: var(--fa-fw-width, 1.25em); }
.fa-layers svg.svg-inline--fa {
bottom: 0;
left: 0;
margin: auto;
position: absolute;
right: 0;
top: 0; }
.fa-layers-text, .fa-layers-counter {
display: inline-block;
position: absolute;
text-align: center; }
.fa-layers {
display: inline-block;
height: 1em;
position: relative;
text-align: center;
vertical-align: -.125em;
width: 1em; }
.fa-layers svg.svg-inline--fa {
-webkit-transform-origin: center center;
transform-origin: center center; }
.fa-layers-text {
left: 50%;
top: 50%;
-webkit-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
-webkit-transform-origin: center center;
transform-origin: center center; }
.fa-layers-counter {
background-color: var(--fa-counter-background-color, #ff253a);
border-radius: var(--fa-counter-border-radius, 1em);
box-sizing: border-box;
color: var(--fa-inverse, #fff);
line-height: var(--fa-counter-line-height, 1);
max-width: var(--fa-counter-max-width, 5em);
min-width: var(--fa-counter-min-width, 1.5em);
overflow: hidden;
padding: var(--fa-counter-padding, 0.25em 0.5em);
right: var(--fa-right, 0);
text-overflow: ellipsis;
top: var(--fa-top, 0);
-webkit-transform: scale(var(--fa-counter-scale, 0.25));
transform: scale(var(--fa-counter-scale, 0.25));
-webkit-transform-origin: top right;
transform-origin: top right; }
.fa-layers-bottom-right {
bottom: var(--fa-bottom, 0);
right: var(--fa-right, 0);
top: auto;
-webkit-transform: scale(var(--fa-layers-scale, 0.25));
transform: scale(var(--fa-layers-scale, 0.25));
-webkit-transform-origin: bottom right;
transform-origin: bottom right; }
.fa-layers-bottom-left {
bottom: var(--fa-bottom, 0);
left: var(--fa-left, 0);
right: auto;
top: auto;
-webkit-transform: scale(var(--fa-layers-scale, 0.25));
transform: scale(var(--fa-layers-scale, 0.25));
-webkit-transform-origin: bottom left;
transform-origin: bottom left; }
.fa-layers-top-right {
top: var(--fa-top, 0);
right: var(--fa-right, 0);
-webkit-transform: scale(var(--fa-layers-scale, 0.25));
transform: scale(var(--fa-layers-scale, 0.25));
-webkit-transform-origin: top right;
transform-origin: top right; }
.fa-layers-top-left {
left: var(--fa-left, 0);
right: auto;
top: var(--fa-top, 0);
-webkit-transform: scale(var(--fa-layers-scale, 0.25));
transform: scale(var(--fa-layers-scale, 0.25));
-webkit-transform-origin: top left;
transform-origin: top left; }
.fa-1x {
font-size: 1em; }
.fa-2x {
font-size: 2em; }
.fa-3x {
font-size: 3em; }
.fa-4x {
font-size: 4em; }
.fa-5x {
font-size: 5em; }
.fa-6x {
font-size: 6em; }
.fa-7x {
font-size: 7em; }
.fa-8x {
font-size: 8em; }
.fa-9x {
font-size: 9em; }
.fa-10x {
font-size: 10em; }
.fa-2xs {
font-size: 0.625em;
line-height: 0.1em;
vertical-align: 0.225em; }
.fa-xs {
font-size: 0.75em;
line-height: 0.08333em;
vertical-align: 0.125em; }
.fa-sm {
font-size: 0.875em;
line-height: 0.07143em;
vertical-align: 0.05357em; }
.fa-lg {
font-size: 1.25em;
line-height: 0.05em;
vertical-align: -0.075em; }
.fa-xl {
font-size: 1.5em;
line-height: 0.04167em;
vertical-align: -0.125em; }
.fa-2xl {
font-size: 2em;
line-height: 0.03125em;
vertical-align: -0.1875em; }
.fa-fw {
text-align: center;
width: 1.25em; }
.fa-ul {
list-style-type: none;
margin-left: var(--fa-li-margin, 2.5em);
padding-left: 0; }
.fa-ul > li {
position: relative; }
.fa-li {
left: calc(var(--fa-li-width, 2em) * -1);
position: absolute;
text-align: center;
width: var(--fa-li-width, 2em);
line-height: inherit; }
.fa-border {
border-color: var(--fa-border-color, #eee);
border-radius: var(--fa-border-radius, 0.1em);
border-style: var(--fa-border-style, solid);
border-width: var(--fa-border-width, 0.08em);
padding: var(--fa-border-padding, 0.2em 0.25em 0.15em); }
.fa-pull-left {
float: left;
margin-right: var(--fa-pull-margin, 0.3em); }
.fa-pull-right {
float: right;
margin-left: var(--fa-pull-margin, 0.3em); }
.fa-beat {
-webkit-animation-name: fa-beat;
animation-name: fa-beat;
-webkit-animation-delay: var(--fa-animation-delay, 0s);
animation-delay: var(--fa-animation-delay, 0s);
-webkit-animation-direction: var(--fa-animation-direction, normal);
animation-direction: var(--fa-animation-direction, normal);
-webkit-animation-duration: var(--fa-animation-duration, 1s);
animation-duration: var(--fa-animation-duration, 1s);
-webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite);
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
-webkit-animation-timing-function: var(--fa-animation-timing, ease-in-out);
animation-timing-function: var(--fa-animation-timing, ease-in-out); }
.fa-bounce {
-webkit-animation-name: fa-bounce;
animation-name: fa-bounce;
-webkit-animation-delay: var(--fa-animation-delay, 0s);
animation-delay: var(--fa-animation-delay, 0s);
-webkit-animation-direction: var(--fa-animation-direction, normal);
animation-direction: var(--fa-animation-direction, normal);
-webkit-animation-duration: var(--fa-animation-duration, 1s);
animation-duration: var(--fa-animation-duration, 1s);
-webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite);
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
-webkit-animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.28, 0.84, 0.42, 1));
animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.28, 0.84, 0.42, 1)); }
.fa-fade {
-webkit-animation-name: fa-fade;
animation-name: fa-fade;
-webkit-animation-delay: var(--fa-animation-delay, 0s);
animation-delay: var(--fa-animation-delay, 0s);
-webkit-animation-direction: var(--fa-animation-direction, normal);
animation-direction: var(--fa-animation-direction, normal);
-webkit-animation-duration: var(--fa-animation-duration, 1s);
animation-duration: var(--fa-animation-duration, 1s);
-webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite);
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
-webkit-animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1));
animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1)); }
.fa-beat-fade {
-webkit-animation-name: fa-beat-fade;
animation-name: fa-beat-fade;
-webkit-animation-delay: var(--fa-animation-delay, 0s);
animation-delay: var(--fa-animation-delay, 0s);
-webkit-animation-direction: var(--fa-animation-direction, normal);
animation-direction: var(--fa-animation-direction, normal);
-webkit-animation-duration: var(--fa-animation-duration, 1s);
animation-duration: var(--fa-animation-duration, 1s);
-webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite);
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
-webkit-animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1));
animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1)); }
.fa-flip {
-webkit-animation-name: fa-flip;
animation-name: fa-flip;
-webkit-animation-delay: var(--fa-animation-delay, 0s);
animation-delay: var(--fa-animation-delay, 0s);
-webkit-animation-direction: var(--fa-animation-direction, normal);
animation-direction: var(--fa-animation-direction, normal);
-webkit-animation-duration: var(--fa-animation-duration, 1s);
animation-duration: var(--fa-animation-duration, 1s);
-webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite);
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
-webkit-animation-timing-function: var(--fa-animation-timing, ease-in-out);
animation-timing-function: var(--fa-animation-timing, ease-in-out); }
.fa-shake {
-webkit-animation-name: fa-shake;
animation-name: fa-shake;
-webkit-animation-delay: var(--fa-animation-delay, 0s);
animation-delay: var(--fa-animation-delay, 0s);
-webkit-animation-direction: var(--fa-animation-direction, normal);
animation-direction: var(--fa-animation-direction, normal);
-webkit-animation-duration: var(--fa-animation-duration, 1s);
animation-duration: var(--fa-animation-duration, 1s);
-webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite);
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
-webkit-animation-timing-function: var(--fa-animation-timing, linear);
animation-timing-function: var(--fa-animation-timing, linear); }
.fa-spin {
-webkit-animation-name: fa-spin;
animation-name: fa-spin;
-webkit-animation-delay: var(--fa-animation-delay, 0s);
animation-delay: var(--fa-animation-delay, 0s);
-webkit-animation-direction: var(--fa-animation-direction, normal);
animation-direction: var(--fa-animation-direction, normal);
-webkit-animation-duration: var(--fa-animation-duration, 2s);
animation-duration: var(--fa-animation-duration, 2s);
-webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite);
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
-webkit-animation-timing-function: var(--fa-animation-timing, linear);
animation-timing-function: var(--fa-animation-timing, linear); }
.fa-spin-reverse {
--fa-animation-direction: reverse; }
.fa-pulse,
.fa-spin-pulse {
-webkit-animation-name: fa-spin;
animation-name: fa-spin;
-webkit-animation-direction: var(--fa-animation-direction, normal);
animation-direction: var(--fa-animation-direction, normal);
-webkit-animation-duration: var(--fa-animation-duration, 1s);
animation-duration: var(--fa-animation-duration, 1s);
-webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite);
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
-webkit-animation-timing-function: var(--fa-animation-timing, steps(8));
animation-timing-function: var(--fa-animation-timing, steps(8)); }
@media (prefers-reduced-motion: reduce) {
.fa-beat,
.fa-bounce,
.fa-fade,
.fa-beat-fade,
.fa-flip,
.fa-pulse,
.fa-shake,
.fa-spin,
.fa-spin-pulse {
-webkit-animation-delay: -1ms;
animation-delay: -1ms;
-webkit-animation-duration: 1ms;
animation-duration: 1ms;
-webkit-animation-iteration-count: 1;
animation-iteration-count: 1;
-webkit-transition-delay: 0s;
transition-delay: 0s;
-webkit-transition-duration: 0s;
transition-duration: 0s; } }
@-webkit-keyframes fa-beat {
0%, 90% {
-webkit-transform: scale(1);
transform: scale(1); }
45% {
-webkit-transform: scale(var(--fa-beat-scale, 1.25));
transform: scale(var(--fa-beat-scale, 1.25)); } }
@keyframes fa-beat {
0%, 90% {
-webkit-transform: scale(1);
transform: scale(1); }
45% {
-webkit-transform: scale(var(--fa-beat-scale, 1.25));
transform: scale(var(--fa-beat-scale, 1.25)); } }
@-webkit-keyframes fa-bounce {
0% {
-webkit-transform: scale(1, 1) translateY(0);
transform: scale(1, 1) translateY(0); }
10% {
-webkit-transform: scale(var(--fa-bounce-start-scale-x, 1.1), var(--fa-bounce-start-scale-y, 0.9)) translateY(0);
transform: scale(var(--fa-bounce-start-scale-x, 1.1), var(--fa-bounce-start-scale-y, 0.9)) translateY(0); }
30% {
-webkit-transform: scale(var(--fa-bounce-jump-scale-x, 0.9), var(--fa-bounce-jump-scale-y, 1.1)) translateY(var(--fa-bounce-height, -0.5em));
transform: scale(var(--fa-bounce-jump-scale-x, 0.9), var(--fa-bounce-jump-scale-y, 1.1)) translateY(var(--fa-bounce-height, -0.5em)); }
50% {
-webkit-transform: scale(var(--fa-bounce-land-scale-x, 1.05), var(--fa-bounce-land-scale-y, 0.95)) translateY(0);
transform: scale(var(--fa-bounce-land-scale-x, 1.05), var(--fa-bounce-land-scale-y, 0.95)) translateY(0); }
57% {
-webkit-transform: scale(1, 1) translateY(var(--fa-bounce-rebound, -0.125em));
transform: scale(1, 1) translateY(var(--fa-bounce-rebound, -0.125em)); }
64% {
-webkit-transform: scale(1, 1) translateY(0);
transform: scale(1, 1) translateY(0); }
100% {
-webkit-transform: scale(1, 1) translateY(0);
transform: scale(1, 1) translateY(0); } }
@keyframes fa-bounce {
0% {
-webkit-transform: scale(1, 1) translateY(0);
transform: scale(1, 1) translateY(0); }
10% {
-webkit-transform: scale(var(--fa-bounce-start-scale-x, 1.1), var(--fa-bounce-start-scale-y, 0.9)) translateY(0);
transform: scale(var(--fa-bounce-start-scale-x, 1.1), var(--fa-bounce-start-scale-y, 0.9)) translateY(0); }
30% {
-webkit-transform: scale(var(--fa-bounce-jump-scale-x, 0.9), var(--fa-bounce-jump-scale-y, 1.1)) translateY(var(--fa-bounce-height, -0.5em));
transform: scale(var(--fa-bounce-jump-scale-x, 0.9), var(--fa-bounce-jump-scale-y, 1.1)) translateY(var(--fa-bounce-height, -0.5em)); }
50% {
-webkit-transform: scale(var(--fa-bounce-land-scale-x, 1.05), var(--fa-bounce-land-scale-y, 0.95)) translateY(0);
transform: scale(var(--fa-bounce-land-scale-x, 1.05), var(--fa-bounce-land-scale-y, 0.95)) translateY(0); }
57% {
-webkit-transform: scale(1, 1) translateY(var(--fa-bounce-rebound, -0.125em));
transform: scale(1, 1) translateY(var(--fa-bounce-rebound, -0.125em)); }
64% {
-webkit-transform: scale(1, 1) translateY(0);
transform: scale(1, 1) translateY(0); }
100% {
-webkit-transform: scale(1, 1) translateY(0);
transform: scale(1, 1) translateY(0); } }
@-webkit-keyframes fa-fade {
50% {
opacity: var(--fa-fade-opacity, 0.4); } }
@keyframes fa-fade {
50% {
opacity: var(--fa-fade-opacity, 0.4); } }
@-webkit-keyframes fa-beat-fade {
0%, 100% {
opacity: var(--fa-beat-fade-opacity, 0.4);
-webkit-transform: scale(1);
transform: scale(1); }
50% {
opacity: 1;
-webkit-transform: scale(var(--fa-beat-fade-scale, 1.125));
transform: scale(var(--fa-beat-fade-scale, 1.125)); } }
@keyframes fa-beat-fade {
0%, 100% {
opacity: var(--fa-beat-fade-opacity, 0.4);
-webkit-transform: scale(1);
transform: scale(1); }
50% {
opacity: 1;
-webkit-transform: scale(var(--fa-beat-fade-scale, 1.125));
transform: scale(var(--fa-beat-fade-scale, 1.125)); } }
@-webkit-keyframes fa-flip {
50% {
-webkit-transform: rotate3d(var(--fa-flip-x, 0), var(--fa-flip-y, 1), var(--fa-flip-z, 0), var(--fa-flip-angle, -180deg));
transform: rotate3d(var(--fa-flip-x, 0), var(--fa-flip-y, 1), var(--fa-flip-z, 0), var(--fa-flip-angle, -180deg)); } }
@keyframes fa-flip {
50% {
-webkit-transform: rotate3d(var(--fa-flip-x, 0), var(--fa-flip-y, 1), var(--fa-flip-z, 0), var(--fa-flip-angle, -180deg));
transform: rotate3d(var(--fa-flip-x, 0), var(--fa-flip-y, 1), var(--fa-flip-z, 0), var(--fa-flip-angle, -180deg)); } }
@-webkit-keyframes fa-shake {
0% {
-webkit-transform: rotate(-15deg);
transform: rotate(-15deg); }
4% {
-webkit-transform: rotate(15deg);
transform: rotate(15deg); }
8%, 24% {
-webkit-transform: rotate(-18deg);
transform: rotate(-18deg); }
12%, 28% {
-webkit-transform: rotate(18deg);
transform: rotate(18deg); }
16% {
-webkit-transform: rotate(-22deg);
transform: rotate(-22deg); }
20% {
-webkit-transform: rotate(22deg);
transform: rotate(22deg); }
32% {
-webkit-transform: rotate(-12deg);
transform: rotate(-12deg); }
36% {
-webkit-transform: rotate(12deg);
transform: rotate(12deg); }
40%, 100% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg); } }
@keyframes fa-shake {
0% {
-webkit-transform: rotate(-15deg);
transform: rotate(-15deg); }
4% {
-webkit-transform: rotate(15deg);
transform: rotate(15deg); }
8%, 24% {
-webkit-transform: rotate(-18deg);
transform: rotate(-18deg); }
12%, 28% {
-webkit-transform: rotate(18deg);
transform: rotate(18deg); }
16% {
-webkit-transform: rotate(-22deg);
transform: rotate(-22deg); }
20% {
-webkit-transform: rotate(22deg);
transform: rotate(22deg); }
32% {
-webkit-transform: rotate(-12deg);
transform: rotate(-12deg); }
36% {
-webkit-transform: rotate(12deg);
transform: rotate(12deg); }
40%, 100% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg); } }
@-webkit-keyframes fa-spin {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg); }
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg); } }
@keyframes fa-spin {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg); }
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg); } }
.fa-rotate-90 {
-webkit-transform: rotate(90deg);
transform: rotate(90deg); }
.fa-rotate-180 {
-webkit-transform: rotate(180deg);
transform: rotate(180deg); }
.fa-rotate-270 {
-webkit-transform: rotate(270deg);
transform: rotate(270deg); }
.fa-flip-horizontal {
-webkit-transform: scale(-1, 1);
transform: scale(-1, 1); }
.fa-flip-vertical {
-webkit-transform: scale(1, -1);
transform: scale(1, -1); }
.fa-flip-both,
.fa-flip-horizontal.fa-flip-vertical {
-webkit-transform: scale(-1, -1);
transform: scale(-1, -1); }
.fa-rotate-by {
-webkit-transform: rotate(var(--fa-rotate-angle, none));
transform: rotate(var(--fa-rotate-angle, none)); }
.fa-stack {
display: inline-block;
vertical-align: middle;
height: 2em;
position: relative;
width: 2.5em; }
.fa-stack-1x,
.fa-stack-2x {
bottom: 0;
left: 0;
margin: auto;
position: absolute;
right: 0;
top: 0;
z-index: var(--fa-stack-z-index, auto); }
.svg-inline--fa.fa-stack-1x {
height: 1em;
width: 1.25em; }
.svg-inline--fa.fa-stack-2x {
height: 2em;
width: 2.5em; }
.fa-inverse {
color: var(--fa-inverse, #fff); }
.sr-only,
.fa-sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0; }
.sr-only-focusable:not(:focus),
.fa-sr-only-focusable:not(:focus) {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0; }
.svg-inline--fa .fa-primary {
fill: var(--fa-primary-color, currentColor);
opacity: var(--fa-primary-opacity, 1); }
.svg-inline--fa .fa-secondary {
fill: var(--fa-secondary-color, currentColor);
opacity: var(--fa-secondary-opacity, 0.4); }
.svg-inline--fa.fa-swap-opacity .fa-primary {
opacity: var(--fa-secondary-opacity, 0.4); }
.svg-inline--fa.fa-swap-opacity .fa-secondary {
opacity: var(--fa-primary-opacity, 1); }
.svg-inline--fa mask .fa-primary,
.svg-inline--fa mask .fa-secondary {
fill: black; }
.fad.fa-inverse,
.fa-duotone.fa-inverse {
color: var(--fa-inverse, #fff); }

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