mirror of https://github.com/MISP/misp-modules
Merge branch 'main' of github.com:MISP/misp-modules
commit
ea22f7bd9d
|
@ -19,4 +19,6 @@ site*
|
|||
venv*
|
||||
|
||||
#vscode
|
||||
.vscode*
|
||||
.vscode*
|
||||
*.sqlite
|
||||
website/conf/config.cfg
|
||||
|
|
1
Pipfile
1
Pipfile
|
@ -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"
|
||||
|
|
11
README.md
11
README.md
|
@ -3,10 +3,12 @@
|
|||
[](https://github.com/MISP/misp-modules/actions/workflows/python-package.yml)[](https://coveralls.io/github/MISP/misp-modules?branch=main)
|
||||
[](https://codecov.io/gh/MISP/misp-modules)
|
||||
|
||||
MISP modules are autonomous modules that can be used to extend [MISP](https://github.com/MISP/MISP) for new services such as expansion, import and export.
|
||||
MISP modules are autonomous modules that can be used to extend [MISP](https://github.com/MISP/MISP) for new services such as expansion, import, export and workflow action.
|
||||
|
||||
MISP modules can be also installed and used without MISP as a [standalone tool accessible via a convenient web interface](./website).
|
||||
|
||||
The modules are written in Python 3 following a simple API interface. The objective is to ease the extensions of MISP functionalities
|
||||
without modifying core components. The API is available via a simple REST API which is independent from MISP installation or configuration.
|
||||
without modifying core components. The API is available via a simple REST API which is independent from MISP installation or configuration and can be used with other tools.
|
||||
|
||||
For more information: [Extending MISP with Python modules](https://www.misp-project.org/misp-training/3.1-misp-modules.pdf) slides from [MISP training](https://github.com/MISP/misp-training).
|
||||
|
||||
|
@ -43,8 +45,10 @@ For more information: [Extending MISP with Python modules](https://www.misp-proj
|
|||
* [GeoIP](misp_modules/modules/expansion/geoip_country.py) - a hover and expansion module to get GeoIP information from geolite/maxmind.
|
||||
* [GeoIP_City](misp_modules/modules/expansion/geoip_city.py) - a hover and expansion module to get GeoIP City information from geolite/maxmind.
|
||||
* [GeoIP_ASN](misp_modules/modules/expansion/geoip_asn.py) - a hover and expansion module to get GeoIP ASN information from geolite/maxmind.
|
||||
* [Google Threat Intelligence] (https://github.com/MISP/misp-modules/tree/main/misp_modules/modules/expansion/google_threat_intelligence.py) - An expansion module to have the observable's threat score assessed by Google Threat Intelligence.
|
||||
* [GreyNoise](misp_modules/modules/expansion/greynoise.py) - a hover and expansion module to get IP and CVE information from GreyNoise.
|
||||
* [hashdd](misp_modules/modules/expansion/hashdd.py) - a hover module to check file hashes against [hashdd.com](http://www.hashdd.com) including NSLR dataset.
|
||||
* [Hashlookup](misp_modules/modules/expansion/hashlookup.py) - An expansion module to enrich a file hash with hashlookup.circl.lu services (NSRL and other sources)
|
||||
* [hibp](misp_modules/modules/expansion/hibp.py) - a hover module to lookup against Have I Been Pwned?
|
||||
* [html_to_markdown](misp_modules/modules/expansion/html_to_markdown.py) - Simple HTML to markdown converter
|
||||
* [HYAS Insight](misp_modules/modules/expansion/hyasinsight.py) - a hover and expansion module to get information from [HYAS Insight](https://www.hyas.com/hyas-insight).
|
||||
|
@ -82,6 +86,7 @@ For more information: [Extending MISP with Python modules](https://www.misp-proj
|
|||
* [Socialscan](misp_modules/modules/expansion/socialscan.py) - a hover module to check if an email address or a username is used on different online platforms, using the [socialscan](https://github.com/iojw/socialscan) python library
|
||||
* [SophosLabs Intelix](misp_modules/modules/expansion/sophoslabs_intelix.py) - SophosLabs Intelix is an API for Threat Intelligence and Analysis (free tier available). [SophosLabs](https://aws.amazon.com/marketplace/pp/B07SLZPMCS)
|
||||
* [sourcecache](misp_modules/modules/expansion/sourcecache.py) - a module to cache a specific link from a MISP instance.
|
||||
* [stairwell](misp_modules/modules/expansion/stairwell.py) - an expansion module to enrich hash observables with the Stairwell API
|
||||
* [STIX2 pattern syntax validator](misp_modules/modules/expansion/stix2_pattern_syntax_validator.py) - a module to check a STIX2 pattern syntax.
|
||||
* [ThreatCrowd](misp_modules/modules/expansion/threatcrowd.py) - an expansion module for [ThreatCrowd](https://www.threatcrowd.org/).
|
||||
* [threatminer](misp_modules/modules/expansion/threatminer.py) - an expansion module to expand from [ThreatMiner](https://www.threatminer.org/).
|
||||
|
@ -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
|
||||
|
|
17
REQUIREMENTS
17
REQUIREMENTS
|
@ -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
|
||||
|
|
|
@ -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 |
Binary file not shown.
After Width: | Height: | Size: 9.0 KiB |
Binary file not shown.
After Width: | Height: | Size: 32 KiB |
|
@ -655,6 +655,27 @@ Module to query a local copy of Maxmind's Geolite database.
|
|||
|
||||
-----
|
||||
|
||||
#### [google_threat_intelligence](https://github.com/MISP/misp-modules/tree/main/misp_modules/modules/expansion/google_threat_intelligence.py)
|
||||
|
||||
<img src=logos/google_threat_intelligence.png height=60>
|
||||
|
||||
An expansion module to have the observable's threat score assessed by Google Threat Intelligence.
|
||||
- **features**:
|
||||
>GTI assessment for the given observable, this include information about level of severity, a clear verdict (malicious, suspicious, undetected and bening) and additional information provided by the Mandiant expertise combined with the VirusTotal database.
|
||||
>
|
||||
>[Output example screeshot](https://github.com/MISP/MISP/assets/4747608/e275db2f-bb1e-4413-8cc0-ec3cb05e0414)
|
||||
- **input**:
|
||||
>A domain, hash (md5, sha1, sha256 or sha512), hostname or IP address attribute.
|
||||
- **output**:
|
||||
>Text fields containing the threat score, the severity, the verdict and the threat label of the observable inspected.
|
||||
- **references**:
|
||||
> - https://www.virustotal.com/
|
||||
> - https://gtidocs.virustotal.com/reference
|
||||
- **requirements**:
|
||||
>An access to the Google Threat Intelligence API (apikey), with a high request rate limit.
|
||||
|
||||
-----
|
||||
|
||||
#### [greynoise](https://github.com/MISP/misp-modules/tree/main/misp_modules/modules/expansion/greynoise.py)
|
||||
|
||||
<img src=logos/greynoise.png height=60>
|
||||
|
@ -1582,6 +1603,25 @@ Module to cache web pages of analysis reports, OSINT sources. The module returns
|
|||
|
||||
-----
|
||||
|
||||
#### [stairwell](https://github.com/MISP/misp-modules/tree/main/misp_modules/modules/expansion/stairwell.py)
|
||||
|
||||
<img src=logos/stairwell.png height=60>
|
||||
|
||||
Module to query the Stairwell API to get additional information about the input hash attribute
|
||||
- **features**:
|
||||
>The module takes a hash attribute as input and queries Stariwell's API to fetch additional data about it. The result, if the payload is observed in Stariwell, is a file object describing the file the input hash is related to.
|
||||
- **input**:
|
||||
>A hash attribute (md5, sha1, sha256).
|
||||
- **output**:
|
||||
>File object related to the input attribute found on Stairwell platform.
|
||||
- **references**:
|
||||
> - https://stairwell.com
|
||||
> - https://docs.stairwell.com
|
||||
- **requirements**:
|
||||
>Access to Stairwell platform (apikey)
|
||||
|
||||
-----
|
||||
|
||||
#### [stix2_pattern_syntax_validator](https://github.com/MISP/misp-modules/tree/main/misp_modules/modules/expansion/stix2_pattern_syntax_validator.py)
|
||||
|
||||
<img src=logos/stix.png height=60>
|
||||
|
@ -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 |
|
@ -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).
|
||||
|
|
|
@ -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)"
|
||||
}
|
|
@ -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."
|
||||
}
|
|
@ -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
|
|
@ -1 +1 @@
|
|||
__all__ = ['testaction', 'mattermost']
|
||||
__all__ = ['testaction', 'mattermost', 'slack']
|
||||
|
|
|
@ -25,6 +25,7 @@ moduleconfig = {
|
|||
'type': 'large_string',
|
||||
'description': 'The template to be used to generate the message to be posted',
|
||||
'value': 'The **template** will be rendered using *Jinja2*!',
|
||||
'jinja_supported': True,
|
||||
},
|
||||
},
|
||||
# Blocking modules break the exection of the current of action
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
import json
|
||||
from slack_sdk import WebClient
|
||||
from slack_sdk.errors import SlackApiError
|
||||
from ._utils import utils
|
||||
|
||||
misperrors = {'error': 'Error'}
|
||||
|
||||
# config fields that your code expects from the site admin
|
||||
moduleconfig = {
|
||||
'params': {
|
||||
'slack_bot_token': {
|
||||
'type': 'string',
|
||||
'description': 'The Slack bot token generated when you created the bot account',
|
||||
},
|
||||
'channel_id': {
|
||||
'type': 'string',
|
||||
'description': 'The channel ID you want to post messages to',
|
||||
},
|
||||
'message_template': {
|
||||
'type': 'large_string',
|
||||
'description': 'The template to be used to generate the message to be posted',
|
||||
'value': 'The **template** will be rendered using *Jinja2*!',
|
||||
'jinja_supported': True,
|
||||
},
|
||||
},
|
||||
# Blocking modules break the execution of the current action
|
||||
'blocking': False,
|
||||
# Indicates whether parts of the data passed to this module should be filtered.
|
||||
'support_filters': True,
|
||||
# Indicates whether the data passed to this module should be compliant with the MISP core format
|
||||
'expect_misp_core_format': False,
|
||||
}
|
||||
|
||||
# returns either "boolean" or "data"
|
||||
# Boolean is used to simply signal that the execution has finished.
|
||||
# For blocking modules, the actual boolean value determines whether we break execution
|
||||
returns = 'boolean'
|
||||
|
||||
moduleinfo = {'version': '0.1', 'author': 'goodlandsecurity',
|
||||
'description': 'Simplistic module to send messages to a Slack channel.',
|
||||
'module-type': ['action']}
|
||||
|
||||
|
||||
def create_post(request):
|
||||
params = request['params']
|
||||
slack_token = params['slack_bot_token']
|
||||
channel_id = params['channel_id']
|
||||
|
||||
client = WebClient(token=slack_token)
|
||||
|
||||
data = request.get('matchingData', request.get('data', {}))
|
||||
|
||||
if params['message_template']:
|
||||
message = utils.renderTemplate(data, params['message_template'])
|
||||
else:
|
||||
message = '```\n{}\n```'.format(json.dumps(data))
|
||||
|
||||
try:
|
||||
client.chat_postMessage(channel=channel_id, text=message)
|
||||
return True
|
||||
except SlackApiError as e:
|
||||
error_message = e.response['error']
|
||||
print(f"Error posting message: {error_message}")
|
||||
return False
|
||||
|
||||
|
||||
def handler(q=False):
|
||||
if q is False:
|
||||
return False
|
||||
request = json.loads(q)
|
||||
create_post(request)
|
||||
return {"data": True}
|
||||
|
||||
|
||||
def introspection():
|
||||
modulesetup = {}
|
||||
try:
|
||||
modulesetup['config'] = moduleconfig
|
||||
except NameError:
|
||||
pass
|
||||
return modulesetup
|
||||
|
||||
|
||||
def version():
|
||||
moduleinfo['config'] = moduleconfig
|
||||
return moduleinfo
|
|
@ -20,7 +20,8 @@ __all__ = ['cuckoo_submit', 'vmray_submit', 'bgpranking', 'circl_passivedns', 'c
|
|||
'trustar_enrich', 'recordedfuture', 'html_to_markdown', 'socialscan', 'passive-ssh',
|
||||
'qintel_qsentry', 'mwdb', 'hashlookup', 'mmdb_lookup', 'ipqs_fraud_and_risk_scoring',
|
||||
'clamav', 'jinja_template_rendering','hyasinsight', 'variotdbs', 'crowdsec',
|
||||
'extract_url_components', 'ipinfo', 'whoisfreaks', 'ip2locationio', 'vysion']
|
||||
'extract_url_components', 'ipinfo', 'whoisfreaks', 'ip2locationio', 'vysion', 'stairwell',
|
||||
'google_threat_intelligence']
|
||||
|
||||
|
||||
minimum_required_fields = ('type', 'uuid', 'value')
|
||||
|
|
|
@ -4,7 +4,7 @@ import dns.resolver
|
|||
misperrors = {'error': 'Error'}
|
||||
mispattributes = {'input': ['hostname', 'domain', 'domain|ip'], 'output': ['ip-src',
|
||||
'ip-dst']}
|
||||
moduleinfo = {'version': '0.2', 'author': 'Alexandre Dulaunoy',
|
||||
moduleinfo = {'version': '0.3', 'author': 'Alexandre Dulaunoy',
|
||||
'description': 'Simple DNS expansion service to resolve IP address from MISP attributes',
|
||||
'module-type': ['expansion', 'hover']}
|
||||
|
||||
|
@ -43,8 +43,8 @@ def handler(q=False):
|
|||
except dns.exception.Timeout:
|
||||
misperrors['error'] = "Timeout"
|
||||
return misperrors
|
||||
except Exception:
|
||||
misperrors['error'] = "DNS resolving error"
|
||||
except Exception as e:
|
||||
misperrors['error'] = f'DNS resolving error {e}'
|
||||
return misperrors
|
||||
|
||||
r = {'results': [{'types': mispattributes['output'],
|
||||
|
|
|
@ -0,0 +1,322 @@
|
|||
#!/usr/local/bin/python
|
||||
# Copyright © 2024 The Google Threat Intelligence authors. All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Google Threat Intelligence MISP expansion module."""
|
||||
|
||||
from urllib import parse
|
||||
import vt
|
||||
import pymisp
|
||||
|
||||
|
||||
MISP_ATTRIBUTES = {
|
||||
'input': [
|
||||
'hostname',
|
||||
'domain',
|
||||
'ip-src',
|
||||
'ip-dst',
|
||||
'md5',
|
||||
'sha1',
|
||||
'sha256',
|
||||
'url',
|
||||
],
|
||||
'format': 'misp_standard',
|
||||
}
|
||||
|
||||
MODULE_INFO = {
|
||||
'version': '1',
|
||||
'author': 'Google Threat Intelligence team',
|
||||
'description': ('An expansion module to have the observable\'s threat'
|
||||
' score assessed by Google Threat Intelligence.'),
|
||||
'module-type': ['expansion'],
|
||||
'config': [
|
||||
'apikey',
|
||||
'event_limit',
|
||||
'proxy_host',
|
||||
'proxy_port',
|
||||
'proxy_username',
|
||||
'proxy_password'
|
||||
]
|
||||
}
|
||||
|
||||
DEFAULT_RESULTS_LIMIT = 10
|
||||
|
||||
|
||||
class GoogleThreatIntelligenceParser:
|
||||
"""Main parser class to create the MISP event."""
|
||||
def __init__(self, client: vt.Client, limit: int) -> None:
|
||||
self.client = client
|
||||
self.limit = limit or DEFAULT_RESULTS_LIMIT
|
||||
self.misp_event = pymisp.MISPEvent()
|
||||
self.attribute = pymisp.MISPAttribute()
|
||||
self.parsed_objects = {}
|
||||
self.input_types_mapping = {
|
||||
'ip-src': self.parse_ip,
|
||||
'ip-dst': self.parse_ip,
|
||||
'domain': self.parse_domain,
|
||||
'hostname': self.parse_domain,
|
||||
'md5': self.parse_hash,
|
||||
'sha1': self.parse_hash,
|
||||
'sha256': self.parse_hash,
|
||||
'url': self.parse_url
|
||||
}
|
||||
self.proxies = None
|
||||
|
||||
def query_api(self, attribute: dict) -> None:
|
||||
"""Get data from the API and parse it."""
|
||||
self.attribute.from_dict(**attribute)
|
||||
self.input_types_mapping[self.attribute.type](self.attribute.value)
|
||||
|
||||
def get_results(self) -> dict:
|
||||
"""Serialize the MISP event."""
|
||||
event = self.misp_event.to_dict()
|
||||
results = {
|
||||
key: event[key] for key in ('Attribute', 'Object') \
|
||||
if (key in event and event[key])
|
||||
}
|
||||
return {'results': results}
|
||||
|
||||
def create_gti_report_object(self, report):
|
||||
"""Create GTI report object."""
|
||||
report = report.to_dict()
|
||||
permalink = ('https://www.virustotal.com/gui/'
|
||||
f"{report['type']}/{report['id']}")
|
||||
report_object = pymisp.MISPObject('Google-Threat-Intel-report')
|
||||
report_object.add_attribute('permalink', type='link', value=permalink)
|
||||
report_object.add_attribute(
|
||||
'Threat Score', type='text',
|
||||
value=get_key(
|
||||
report, 'attributes.gti_assessment.threat_score.value'))
|
||||
report_object.add_attribute(
|
||||
'Verdict', type='text',
|
||||
value=get_key(
|
||||
report, 'attributes.gti_assessment.verdict.value').replace(
|
||||
'VERDICT_', ''))
|
||||
report_object.add_attribute(
|
||||
'Severity', type='text',
|
||||
value=get_key(
|
||||
report, 'attributes.gti_assessment.severity.value').replace(
|
||||
'SEVERITY_', ''))
|
||||
report_object.add_attribute(
|
||||
'Threat Label', type='text',
|
||||
value=get_key(
|
||||
report, ('attributes.popular_threat_classification'
|
||||
'.suggested_threat_label')))
|
||||
self.misp_event.add_object(**report_object)
|
||||
return report_object.uuid
|
||||
|
||||
def parse_domain(self, domain: str) -> str:
|
||||
"""Create domain MISP object."""
|
||||
domain_report = self.client.get_object(f'/domains/{domain}')
|
||||
|
||||
# DOMAIN
|
||||
domain_object = pymisp.MISPObject('domain-ip')
|
||||
domain_object.add_attribute(
|
||||
'domain', type='domain', value=domain_report.id)
|
||||
|
||||
report_uuid = self.create_gti_report_object(domain_report)
|
||||
domain_object.add_reference(report_uuid, 'analyzed-with')
|
||||
self.misp_event.add_object(**domain_object)
|
||||
return domain_object.uuid
|
||||
|
||||
def parse_hash(self, file_hash: str) -> str:
|
||||
"""Create hash MISP object."""
|
||||
file_report = self.client.get_object(f'/files/{file_hash}')
|
||||
file_object = pymisp.MISPObject('file')
|
||||
for hash_type in ('md5', 'sha1', 'sha256'):
|
||||
file_object.add_attribute(
|
||||
hash_type,
|
||||
**{'type': hash_type, 'value': file_report.get(hash_type)})
|
||||
|
||||
report_uuid = self.create_gti_report_object(file_report)
|
||||
file_object.add_reference(report_uuid, 'analyzed-with')
|
||||
self.misp_event.add_object(**file_object)
|
||||
return file_object.uuid
|
||||
|
||||
def parse_ip(self, ip: str) -> str:
|
||||
"""Create ip MISP object."""
|
||||
ip_report = self.client.get_object(f'/ip_addresses/{ip}')
|
||||
|
||||
# IP
|
||||
ip_object = pymisp.MISPObject('domain-ip')
|
||||
ip_object.add_attribute('ip', type='ip-dst', value=ip_report.id)
|
||||
|
||||
report_uuid = self.create_gti_report_object(ip_report)
|
||||
ip_object.add_reference(report_uuid, 'analyzed-with')
|
||||
self.misp_event.add_object(**ip_object)
|
||||
return ip_object.uuid
|
||||
|
||||
def parse_url(self, url: str) -> str:
|
||||
"""Create URL MISP object."""
|
||||
url_id = vt.url_id(url)
|
||||
url_report = self.client.get_object(f'/urls/{url_id}')
|
||||
|
||||
url_object = pymisp.MISPObject('url')
|
||||
url_object.add_attribute('url', type='url', value=url_report.url)
|
||||
|
||||
report_uuid = self.create_gti_report_object(url_report)
|
||||
url_object.add_reference(report_uuid, 'analyzed-with')
|
||||
self.misp_event.add_object(**url_object)
|
||||
return url_object.uuid
|
||||
|
||||
|
||||
def get_key(dictionary, key, default_value=''):
|
||||
"""Get value from nested dictionaries."""
|
||||
dictionary = dictionary or {}
|
||||
keys = key.split('.')
|
||||
field_name = keys.pop()
|
||||
for k in keys:
|
||||
if k not in dictionary:
|
||||
return default_value
|
||||
dictionary = dictionary[k]
|
||||
return dictionary.get(field_name, default_value)
|
||||
|
||||
|
||||
def get_proxy_settings(config: dict) -> dict:
|
||||
"""Returns proxy settings in the requests format or None if not set up."""
|
||||
proxies = None
|
||||
host = config.get('proxy_host')
|
||||
port = config.get('proxy_port')
|
||||
username = config.get('proxy_username')
|
||||
password = config.get('proxy_password')
|
||||
|
||||
if host:
|
||||
if not port:
|
||||
raise KeyError(
|
||||
('The google_threat_intelligence_proxy_host config is set, '
|
||||
'please also set the virustotal_proxy_port.'))
|
||||
parsed = parse.urlparse(host)
|
||||
if 'http' in parsed.scheme:
|
||||
scheme = 'http'
|
||||
else:
|
||||
scheme = parsed.scheme
|
||||
netloc = parsed.netloc
|
||||
host = f'{netloc}:{port}'
|
||||
|
||||
if username:
|
||||
if not password:
|
||||
raise KeyError(('The google_threat_intelligence_'
|
||||
' proxy_host config is set, please also'
|
||||
' set the virustotal_proxy_password.'))
|
||||
auth = f'{username}:{password}'
|
||||
host = auth + '@' + host
|
||||
|
||||
proxies = {
|
||||
'http': f'{scheme}://{host}',
|
||||
'https': f'{scheme}://{host}'
|
||||
}
|
||||
return proxies
|
||||
|
||||
|
||||
def dict_handler(request: dict):
|
||||
"""MISP entry point fo the module."""
|
||||
if not request.get('config') or not request['config'].get('apikey'):
|
||||
return {
|
||||
'error': ('A Google Threat Intelligence api '
|
||||
'key is required for this module.')
|
||||
}
|
||||
|
||||
if not request.get('attribute'):
|
||||
return {
|
||||
'error': ('This module requires an "attribute" field as input,'
|
||||
' which should contain at least a type, a value and an'
|
||||
' uuid.')
|
||||
}
|
||||
|
||||
if request['attribute']['type'] not in MISP_ATTRIBUTES['input']:
|
||||
return {'error': 'Unsupported attribute type.'}
|
||||
|
||||
event_limit = request['config'].get('event_limit')
|
||||
attribute = request['attribute']
|
||||
|
||||
try:
|
||||
proxy_settings = get_proxy_settings(request.get('config'))
|
||||
client = vt.Client(
|
||||
request['config']['apikey'],
|
||||
headers={
|
||||
'x-tool': 'MISPModuleGTIExpansion',
|
||||
},
|
||||
proxy=proxy_settings['http'] if proxy_settings else None)
|
||||
parser = GoogleThreatIntelligenceParser(
|
||||
client, int(event_limit) if event_limit else None)
|
||||
parser.query_api(attribute)
|
||||
except vt.APIError as ex:
|
||||
return {'error': ex.message}
|
||||
except KeyError as ex:
|
||||
return {'error': str(ex)}
|
||||
|
||||
return parser.get_results()
|
||||
|
||||
|
||||
def introspection():
|
||||
"""Returns the module input attributes required."""
|
||||
return MISP_ATTRIBUTES
|
||||
|
||||
|
||||
def version():
|
||||
"""Returns the module metadata."""
|
||||
return MODULE_INFO
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Testing/debug calls.
|
||||
import os
|
||||
api_key = os.getenv('GTI_API_KEY')
|
||||
# File
|
||||
request_data = {
|
||||
'config': {'apikey': api_key},
|
||||
'attribute': {
|
||||
'type': 'sha256',
|
||||
'value': ('ed01ebfbc9eb5bbea545af4d01bf5f10'
|
||||
'71661840480439c6e5babe8e080e41aa')
|
||||
}
|
||||
}
|
||||
response = dict_handler(request_data)
|
||||
report_obj = response['results']['Object'][0]
|
||||
print(report_obj.to_dict())
|
||||
|
||||
# URL
|
||||
request_data = {
|
||||
'config': {'apikey': api_key},
|
||||
'attribute': {
|
||||
'type': 'url',
|
||||
'value': 'http://47.21.48.182:60813/Mozi.a'
|
||||
}
|
||||
}
|
||||
response = dict_handler(request_data)
|
||||
report_obj = response['results']['Object'][0]
|
||||
print(report_obj.to_dict())
|
||||
|
||||
# Ip
|
||||
request_data = {
|
||||
'config': {'apikey': api_key},
|
||||
'attribute': {
|
||||
'type': 'ip-src',
|
||||
'value': '180.72.148.38'
|
||||
}
|
||||
}
|
||||
response = dict_handler(request_data)
|
||||
report_obj = response['results']['Object'][0]
|
||||
print(report_obj.to_dict())
|
||||
|
||||
# Domain
|
||||
request_data = {
|
||||
'config': {'apikey': api_key},
|
||||
'attribute': {
|
||||
'type': 'domain',
|
||||
'value': 'qexyhuv.com'
|
||||
}
|
||||
}
|
||||
response = dict_handler(request_data)
|
||||
report_obj = response['results']['Object'][0]
|
||||
print(report_obj.to_dict())
|
|
@ -6,10 +6,8 @@ from pyipasnhistory import IPASNHistory
|
|||
from pymisp import MISPAttribute, MISPEvent, MISPObject
|
||||
|
||||
misperrors = {'error': 'Error'}
|
||||
mispattributes = {'input': ['ip-src', 'ip-dst'], 'format': 'misp_standard'}
|
||||
moduleinfo = {'version': '0.2', 'author': 'Raphaël Vinot',
|
||||
'description': 'Query an IP ASN history service (https://github.com/CIRCL/IP-ASN-history.git)',
|
||||
'module-type': ['expansion', 'hover']}
|
||||
mispattributes = {'input': ['ip-src', 'ip-dst', 'ip'], 'format': 'misp_standard'}
|
||||
moduleinfo = {'version': '0.3', 'author': 'Raphaël Vinot', 'description': 'Query an IP ASN history service (https://github.com/D4-project/IPASN-History?tab=readme-ov-file)', 'module-type': ['expansion', 'hover']}
|
||||
|
||||
|
||||
def parse_result(attribute, values):
|
||||
|
@ -18,7 +16,6 @@ def parse_result(attribute, values):
|
|||
initial_attribute.from_dict(**attribute)
|
||||
event.add_attribute(**initial_attribute)
|
||||
mapping = {'asn': ('AS', 'asn'), 'prefix': ('ip-src', 'subnet-announced')}
|
||||
print(values)
|
||||
for last_seen, response in values['response'].items():
|
||||
asn = MISPObject('asn')
|
||||
asn.add_attribute('last-seen', **{'type': 'datetime', 'value': last_seen})
|
||||
|
@ -39,6 +36,9 @@ def handler(q=False):
|
|||
return {'error': f'{standard_error_message}, which should contain at least a type, a value and an uuid.'}
|
||||
if request['attribute']['type'] not in mispattributes['input']:
|
||||
return {'error': 'Unsupported attribute type.'}
|
||||
if request['attribute']['type'] == 'ip':
|
||||
request['attribute']['type'] = 'ip-src'
|
||||
|
||||
toquery = request['attribute']['value']
|
||||
|
||||
ipasn = IPASNHistory()
|
||||
|
|
|
@ -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
|
|
@ -4,11 +4,11 @@ from . import check_input_attribute, standard_error_message
|
|||
from pymisp import MISPAttribute, MISPEvent, MISPObject
|
||||
|
||||
misperrors = {'error': 'Error'}
|
||||
mispattributes = {'input': ['hostname', 'domain', "ip-src", "ip-dst", "md5", "sha1", "sha256", "url"],
|
||||
mispattributes = {'input': ['hostname', 'domain', "ip-src", "ip-dst", "md5", "sha1", "sha256", "url", "ip-src|port", "ip-dst|port"],
|
||||
'format': 'misp_standard'}
|
||||
|
||||
# possible module-types: 'expansion', 'hover' or both
|
||||
moduleinfo = {'version': '5', 'author': 'Hannah Ward',
|
||||
moduleinfo = {'version': '6', 'author': 'Hannah Ward',
|
||||
'description': 'Enrich observables with the VirusTotal v3 API',
|
||||
'module-type': ['expansion']}
|
||||
|
||||
|
@ -29,7 +29,8 @@ class VirusTotalParser:
|
|||
self.input_types_mapping = {'ip-src': self.parse_ip, 'ip-dst': self.parse_ip,
|
||||
'domain': self.parse_domain, 'hostname': self.parse_domain,
|
||||
'md5': self.parse_hash, 'sha1': self.parse_hash,
|
||||
'sha256': self.parse_hash, 'url': self.parse_url}
|
||||
'sha256': self.parse_hash, 'url': self.parse_url,
|
||||
'ip-src|port': self.parse_ip_port, 'ip-dst|port': self.parse_ip_port}
|
||||
self.proxies = None
|
||||
|
||||
@staticmethod
|
||||
|
@ -51,7 +52,11 @@ class VirusTotalParser:
|
|||
def add_vt_report(self, report: vt.Object) -> str:
|
||||
analysis = report.get('last_analysis_stats')
|
||||
total = self.get_total_analysis(analysis, report.get('known_distributors'))
|
||||
permalink = f'https://www.virustotal.com/gui/{report.type}/{report.id}'
|
||||
if report.type == 'ip_address':
|
||||
rtype = 'ip-address'
|
||||
else:
|
||||
rtype = report.type
|
||||
permalink = f'https://www.virustotal.com/gui/{rtype}/{report.id}'
|
||||
|
||||
vt_object = MISPObject('virustotal-report')
|
||||
vt_object.add_attribute('permalink', type='link', value=permalink)
|
||||
|
@ -160,6 +165,9 @@ class VirusTotalParser:
|
|||
|
||||
self.misp_event.add_object(**file_object)
|
||||
return file_object.uuid
|
||||
def parse_ip_port(self, ipport: str) -> str:
|
||||
ip = ipport.split('|')[0]
|
||||
self.parse_ip(ip)
|
||||
|
||||
def parse_ip(self, ip: str) -> str:
|
||||
ip_report = self.client.get_object(f'/ip_addresses/{ip}')
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
# MISP-module website
|
||||
|
||||
Use all modules with a dedicate website without any MISP
|
||||
|
||||

|
||||
|
||||

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