First commit
137
.gitignore
vendored
Normal file
@ -0,0 +1,137 @@
|
||||
# Shitty MacOS stuff
|
||||
.DS_Store
|
||||
._*
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
pip-wheel-metadata/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# Node.js
|
||||
node_modules/
|
||||
npm-debug.log
|
1
LICENSE.txt
Normal file
1
NOTICE.txt
Normal file
145
README.md
Normal file
@ -0,0 +1,145 @@
|
||||
# TinyCheck
|
||||
|
||||
### Description
|
||||
|
||||
TinyCheck allows you to easily capture network communications from a smartphone or any device which can be associated to a Wi-Fi access point in order to quickly analyze them. This can be used to check if any suspect or malicious communication is outgoing from a smartphone, by using heuristics or specific Indicators of Compromise (IoCs).
|
||||
|
||||
![Architecture](/assets/network-home.png)
|
||||
|
||||
In order to make it working, you need a computer with a Debian-like operating system and two Wi-Fi interfaces. The best choice is to use a [Raspberry Pi (3+)](https://www.raspberrypi.org) with a Wi-Fi dongle and a small touch screen. This tiny configuration (for less than \$50) allows you to tap any Wi-Fi device, anywhere.
|
||||
|
||||
### History
|
||||
|
||||
The idea of TinyCheck came to me in a meeting about stalkerwares with a [French women's shelter](https://www.centre-hubertine-auclert.fr). During this meeting we talked about how to easily detect easily [stalkerwares](https://stopstalkerware.org/) without installing very technical apps nor doing forensic analysis on them. The initial concept was to develop a tiny kiosk device based on Raspberry Pi which can be used by non-tech people to test their smartphones against malicious communications issued by stalkerwares or any spyware.
|
||||
|
||||
Of course, TinyCheck can also be used to spot any malicious communications from cybercrime or state-sponsored implants. It allows the end-user to push his own extended Indicators of Compromise via a backend in order to detect some ghosts over the wire.
|
||||
|
||||
### Use cases
|
||||
|
||||
TinyCheck can be used in several ways by individuals and entities:
|
||||
|
||||
- Over a network - TinyCheck is installed on a network and can be accessed from a workstation via a browser.
|
||||
- In kiosk mode - TinyCheck can be used as a kiosk to allow visitors to test their own devices.
|
||||
- Fully standalone - By using a powerbank, you can tap any device anywhere.
|
||||
|
||||
### How to analyze your smartphone
|
||||
|
||||
1. **Disable mobile aka. cellular data**
|
||||
Disable the 3G/4G data link in your smartphone configuration.
|
||||
2. **Connect your smartphone to the WiFi network generated by TinyCheck**
|
||||
Once connected to the Wi-Fi network, its advised to wait like 10-20 minutes.
|
||||
3. **Interact with your smartphone**
|
||||
Send an SMS, make a call, take a photo, restart your phone - some implants might react to such events.
|
||||
4. **Stop the capture**
|
||||
Stop the capture by clicking on the button.
|
||||
5. **Analyze the capture**
|
||||
Analyze the captured communication, enjoy (or not).
|
||||
6. **Save the capture**
|
||||
Save the capture on an USB key or by direct download.
|
||||
|
||||
### Architecture
|
||||
|
||||
TinyCheck is divided in three independent parts:
|
||||
|
||||
- A backend: where the user can add his own extended IOCs, whitelist elements, edit the configuration etc.
|
||||
- A frontend: where the user can analyze the communication of his device by creating an ephemeral WiFi AP.
|
||||
- An analysis engine: used to analyze the pcap by using Zeek, Suricata, extended IOCs and heuristics.
|
||||
|
||||
The backend and the frontend are quite similar. Both consist of a [VueJS](https://vuejs.org/) application (sources stored under `/app/`) and an API endpoint developed in [Flask](https://flask.palletsprojects.com/) (stored under `/server/`). The data shared between the backend and the frontend are stored under the `config.yaml` file for configuration and `tinycheck.sqlite3` database for the whitelist/IOCs.
|
||||
|
||||
It is worthy to note that not all configuration options are editable from the frontend (such as default ports, Free certificates issuers etc.). Don't hesitate to take a look at the `config.yaml` file to tweak some configuration options.
|
||||
|
||||
### Installation
|
||||
|
||||
Prior the TinyCheck installation, you need to have:
|
||||
|
||||
- A Raspberry Pi with [Raspberry Pi OS](https://www.raspberrypi.org/documentation/installation/installing-images/) (or any computer with a Debian-like system)
|
||||
- Two working Wi-Fi interfaces (check their number with `ifconfig | grep wlan | wc -l`).
|
||||
- A working internet connection
|
||||
- (Adviced) A small touchscreen previously installed for the kiosk mode of TinyCheck.
|
||||
|
||||
```console
|
||||
$ cd /tmp/
|
||||
$ git clone https://github.com/KasperskyLab/TinyCheck
|
||||
$ cd TinyCheck
|
||||
$ sudo bash install.sh
|
||||
```
|
||||
|
||||
By executing `install.sh`, all the dependencies associated to the project will be installed and it can take several minutes depending of your internet speed. Four services are going to be created:
|
||||
- `tinycheck-backend` executing the backend server & interface;
|
||||
- `tinycheck-frontend` executing the frontend server & interface;
|
||||
- `tinycheck-kiosk` to handle the kiosk version of TinyCheck;
|
||||
- `tinycheck-watchers` to handle the watchers which update automatically IOCs / whitelist from external URLs;
|
||||
|
||||
Once installed, the operating system is going to reboot.
|
||||
|
||||
### Meet the frontend
|
||||
|
||||
The frontend - which can be accessed from `http://tinycheck.local` - is a kind of tunnel which help the user throughout the process of network capture and reporting. It allows the user to setup a Wi-Fi connection to an existing Wi-Fi network, create an ephemeral Wi-Fi network, capture the communications and show a report to the user... in less than one minute, 5 clicks and without any technical knowledge.
|
||||
|
||||
![Frontend](/assets/frontend.png)
|
||||
|
||||
### Meet the backend
|
||||
|
||||
Once installed, you can connect yourself to the TinyCheck backend by browsing the URL `https://tinycheck.local` and accepting the SSL self-signed certificate. The default credentials are `tinycheck` / `tinycheck`.
|
||||
|
||||
![Backend](/assets/backend.png)
|
||||
|
||||
The backend allows you to edit the configuration of TinyCheck, add extended IOCs and whitelisted elements in order to prevent false positives. Several IOCs are already stored in the database such as few suricata rules, FreeDNS, Name servers, CIDRs known to host malicious servers and so on. In term of extended IOCs, this first version of TinyCheck includes:
|
||||
|
||||
- Suricata rules
|
||||
- CIDRs
|
||||
- Domains & FQDNs (named generically "Domains")
|
||||
- IPv4 / IPv6 Addresses
|
||||
- Certificates sha1
|
||||
- Nameservers
|
||||
- FreeDNS
|
||||
- Fancy TLDs
|
||||
|
||||
### Meet the analysis engine
|
||||
|
||||
The analysis engine is pretty straightforward. For this first version, the network communications are not analyzed in real time during the capture. The engine executes Zeek and Suricata against the previously saved network capture. [Zeek](https://zeek.org/) is a well-known network dissector which stores in several logs the captured session.
|
||||
|
||||
Once saved, these logs are analysed to find extended IOCs (listed above) or to match heuristics rules (which can be deactivated through the backend). The heuristics rules are hardcoded in `zeekengine.py`, and they are listed below. As only one device is analyzed at a time, there is a low probability to see heuristic alerts leveraged.
|
||||
|
||||
- UDP/ICMP going outside the local network
|
||||
- UDP/TCP connection with a destination port >1024
|
||||
- Remote host not resolved by DNS during the session
|
||||
- Use of self-signed certificate by the remote host
|
||||
- SSL connection done on a non standard port
|
||||
- Use of specific SSL certificates issuers by the remote host (such as Let's Encrypt)
|
||||
- HTTP requests done during the session
|
||||
- HTTP requests done on a non standard port
|
||||
- ...
|
||||
|
||||
On the [Suricata](https://suricata-ids.org/) part, the network capture is analysed against suricata rules saved as IOCs. Few rules are dynamics such as:
|
||||
|
||||
- Device name exfiltred in clear-text;
|
||||
- Access point SSID exfiltred in clear-text;
|
||||
|
||||
### Possible updates for next releases
|
||||
|
||||
- Centralized server for IOC/whitelist management (aka. Remote Analysis).
|
||||
- Possibility to add watchers from the backend interface.
|
||||
- Encryption of reports.
|
||||
- Better frontend GUI/JS (use of websockets / better animations).
|
||||
- More OpSec (TOR integration etc.)
|
||||
|
||||
### Special thanks
|
||||
|
||||
**Guys who provided some IOCs**
|
||||
- Cian Heasley for his android stalkerwares IOC repo, available here: https://github.com/diskurse/android-stalkerware
|
||||
- Te-k for his stalkerwares awesome IOCs repo, available here: https://github.com/Te-k/stalkerware-indicators
|
||||
- Emilien for his Stratum rules, available here: https://github.com/kwouffe/cryptonote-hunt
|
||||
- Costin Raiu for his tracker domains, available here: https://github.com/craiu/mobiletrackers/blob/master/list.txt
|
||||
|
||||
**Code review**
|
||||
- Dan Demeter
|
||||
- Maxime Granier
|
||||
- Florian Pires
|
||||
|
||||
**Others**
|
||||
- GReAT colleagues.
|
||||
- Zeek and Suricata awesome maintainers.
|
||||
- virtual-keyboard.js.org guys.
|
||||
- Loading.io guys.
|
0
analysis/__init__.py
Normal file
61
analysis/analysis.py
Normal file
@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from classes.zeekengine import ZeekEngine
|
||||
from classes.suricataengine import SuricataEngine
|
||||
from multiprocessing import Process, Manager
|
||||
import sys
|
||||
import re
|
||||
import json
|
||||
import os
|
||||
|
||||
"""
|
||||
This file is called by the frontend but the analysis
|
||||
can be done in standalone by just submitting the directory
|
||||
containing a capture.pcap file.
|
||||
"""
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) == 2:
|
||||
capture_directory = sys.argv[1]
|
||||
if os.path.isdir(capture_directory):
|
||||
|
||||
# Alerts bucket.
|
||||
manager = Manager()
|
||||
alerts = manager.dict()
|
||||
|
||||
def zeekengine(alerts):
|
||||
zeek = ZeekEngine(capture_directory)
|
||||
zeek.start_zeek()
|
||||
alerts["zeek"] = zeek.get_alerts()
|
||||
|
||||
def snortengine(alerts):
|
||||
suricata = SuricataEngine(capture_directory)
|
||||
suricata.start_suricata()
|
||||
alerts["suricata"] = suricata.get_alerts()
|
||||
|
||||
# Start the engines.
|
||||
p1 = Process(target=zeekengine, args=(alerts,))
|
||||
p2 = Process(target=snortengine, args=(alerts,))
|
||||
p1.start()
|
||||
p2.start()
|
||||
|
||||
# Wait to their end.
|
||||
p1.join()
|
||||
p2.join()
|
||||
|
||||
# Some formating and alerts.json writing.
|
||||
with open(os.path.join(capture_directory, "alerts.json"), "w") as f:
|
||||
report = {"high": [], "moderate": [], "low": []}
|
||||
for alert in (alerts["zeek"] + alerts["suricata"]):
|
||||
if alert["level"] == "High":
|
||||
report["high"].append(alert)
|
||||
if alert["level"] == "Moderate":
|
||||
report["moderate"].append(alert)
|
||||
if alert["level"] == "Low":
|
||||
report["low"].append(alert)
|
||||
f.write(json.dumps(report))
|
||||
else:
|
||||
print("The directory doesn't exist.")
|
||||
else:
|
||||
print("Please specify a capture directory in argument.")
|
178
analysis/classes/parsezeeklogs.py
Normal file
@ -0,0 +1,178 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from json import loads, dumps
|
||||
from collections import OrderedDict
|
||||
from datetime import datetime
|
||||
from traceback import print_exc
|
||||
|
||||
# Taken from https://github.com/dgunter/ParseZeekLogs <3
|
||||
|
||||
|
||||
class ParseZeekLogs(object):
|
||||
"""
|
||||
Class that parses Zeek logs and allows log data to be output in CSV or json format.
|
||||
Attributes: filepath: Path of Zeek log file to read
|
||||
"""
|
||||
|
||||
def __init__(self, filepath, batchsize=500, fields=None, output_format=None, ignore_keys=[], meta={}, safe_headers=False):
|
||||
self.fd = open(filepath, "r")
|
||||
self.options = OrderedDict()
|
||||
self.firstRun = True
|
||||
self.filtered_fields = fields
|
||||
self.batchsize = batchsize
|
||||
self.output_format = output_format
|
||||
self.ignore_keys = ignore_keys
|
||||
self.meta = meta
|
||||
self.safe_headers = safe_headers
|
||||
|
||||
# Convert ' to " in meta string
|
||||
meta = loads(dumps(meta).replace("'", '"'))
|
||||
|
||||
# Read the header option lines
|
||||
l = self.fd.readline().strip()
|
||||
while l.strip().startswith("#"):
|
||||
# Parse the options out
|
||||
if l.startswith("#separator"):
|
||||
key = str(l[1:].split(" ")[0])
|
||||
value = str.encode(l[1:].split(
|
||||
" ")[1].strip()).decode('unicode_escape')
|
||||
self.options[key] = value
|
||||
elif l.startswith("#"):
|
||||
key = str(l[1:].split(self.options.get('separator'))[0])
|
||||
value = l[1:].split(self.options.get('separator'))[1:]
|
||||
self.options[key] = value
|
||||
|
||||
# Read the next line
|
||||
l = self.fd.readline().strip()
|
||||
|
||||
self.firstLine = l
|
||||
|
||||
# Save mapping of fields to values:
|
||||
self.fields = self.options.get('fields')
|
||||
self.types = self.options.get('types')
|
||||
|
||||
self.data_types = {}
|
||||
for i, val in enumerate(self.fields):
|
||||
# Convert field names if safe_headers is enabled
|
||||
if self.safe_headers is True:
|
||||
self.fields[i] = self.fields[i].replace(".", "_")
|
||||
|
||||
# Match types with each other
|
||||
self.data_types[self.fields[i]] = self.types[i]
|
||||
|
||||
def __del__(self):
|
||||
self.fd.close()
|
||||
|
||||
def __iter__(self):
|
||||
return self
|
||||
|
||||
def __next__(self):
|
||||
retVal = ""
|
||||
if self.firstRun is True:
|
||||
retVal = self.firstLine
|
||||
self.firstRun = False
|
||||
else:
|
||||
retVal = self.fd.readline().strip()
|
||||
|
||||
# If an empty string is returned, readline is done reading
|
||||
if retVal == "" or retVal is None:
|
||||
raise StopIteration
|
||||
|
||||
# Split out the data we are going to return
|
||||
retVal = retVal.split(self.options.get('separator'))
|
||||
|
||||
record = None
|
||||
# Make sure we aren't dealing with a comment line
|
||||
if len(retVal) > 0 and not str(retVal[0]).strip().startswith("#") \
|
||||
and len(retVal) is len(self.options.get("fields")):
|
||||
record = OrderedDict()
|
||||
# Prepare fields for conversion
|
||||
for x in range(0, len(retVal)):
|
||||
if self.safe_headers is True:
|
||||
converted_field_name = self.options.get(
|
||||
"fields")[x].replace(".", "_")
|
||||
else:
|
||||
converted_field_name = self.options.get("fields")[x]
|
||||
if self.filtered_fields is None or converted_field_name in self.filtered_fields:
|
||||
# Translate - to "" to fix a conversation error
|
||||
if retVal[x] == "-":
|
||||
retVal[x] = ""
|
||||
# Save the record field if the field isn't filtered out
|
||||
record[converted_field_name] = retVal[x]
|
||||
|
||||
# Convert values to the appropriate record type
|
||||
record = self.convert_values(
|
||||
record, self.ignore_keys, self.data_types)
|
||||
|
||||
if record is not None and self.output_format == "json":
|
||||
# Output will be json
|
||||
|
||||
# Add metadata to json
|
||||
for k, v in self.meta.items():
|
||||
record[k] = v
|
||||
|
||||
retVal = record
|
||||
elif record is not None and self.output_format == "csv":
|
||||
retVal = ""
|
||||
# Add escaping to csv format
|
||||
for k, v in record.items():
|
||||
# Add escaping to string values
|
||||
if isinstance(v, str):
|
||||
retVal += str("\"" + str(v).strip() + "\"" + ",")
|
||||
else:
|
||||
retVal += str(str(v).strip() + ",")
|
||||
# Remove the trailing comma
|
||||
retVal = retVal[:-1]
|
||||
else:
|
||||
retVal = None
|
||||
|
||||
return retVal
|
||||
|
||||
def convert_values(self, data, ignore_keys=[], data_types={}):
|
||||
keys_to_delete = []
|
||||
for k, v in data.items():
|
||||
# print("evaluating k: " + str(k) + " v: " + str(v))
|
||||
|
||||
if isinstance(v, dict):
|
||||
data[k] = self.convert_values(v)
|
||||
else:
|
||||
if data_types.get(k) is not None:
|
||||
if (data_types.get(k) == "port" or data_types.get(k) == "count"):
|
||||
if v != "":
|
||||
data[k] = int(v)
|
||||
else:
|
||||
keys_to_delete.append(k)
|
||||
elif (data_types.get(k) == "double" or data_types.get(k) == "interval"):
|
||||
if v != "":
|
||||
data[k] = float(v)
|
||||
else:
|
||||
keys_to_delete.append(k)
|
||||
elif data_types.get(k) == "bool":
|
||||
data[k] = bool(v)
|
||||
else:
|
||||
data[k] = v
|
||||
|
||||
for k in keys_to_delete:
|
||||
del data[k]
|
||||
|
||||
return data
|
||||
|
||||
def get_fields(self):
|
||||
"""Returns all fields present in the log file
|
||||
Returns:
|
||||
A python list containing all field names in the log file
|
||||
"""
|
||||
field_names = ""
|
||||
if self.output_format == "csv":
|
||||
for i, v in enumerate(self.fields):
|
||||
if self.filtered_fields is None or v in self.filtered_fields:
|
||||
field_names += str(v) + ","
|
||||
# Remove the trailing comma
|
||||
field_names = field_names[:-1].strip()
|
||||
else:
|
||||
field_names = []
|
||||
for i, v in enumerate(self.fields):
|
||||
if self.filtered_fields is None or v in self.filtered_fields:
|
||||
field_names.append(v)
|
||||
return field_names
|
92
analysis/classes/suricataengine.py
Normal file
@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from utils import get_iocs, get_apname, get_device
|
||||
import time
|
||||
import os
|
||||
import subprocess as sp
|
||||
import re
|
||||
import json
|
||||
|
||||
|
||||
class SuricataEngine():
|
||||
|
||||
def __init__(self, capture_directory):
|
||||
|
||||
self.wdir = capture_directory
|
||||
self.alerts = []
|
||||
self.rules_file = "/tmp/rules.rules"
|
||||
self.pcap_path = os.path.join(self.wdir, "capture.pcap")
|
||||
self.rules = [r[0] for r in get_iocs(
|
||||
"snort")] + self.generate_contextual_alerts()
|
||||
|
||||
def start_suricata(self):
|
||||
"""
|
||||
Launch suricata against the capture.pcap file.
|
||||
:return: nothing.
|
||||
"""
|
||||
|
||||
# Generate the rule file an launch suricata.
|
||||
if self.generate_rule_file():
|
||||
sp.Popen("suricata -S {} -r {} -l /tmp/".format(self.rules_file,
|
||||
self.pcap_path), shell=True).wait()
|
||||
|
||||
# Let's parse the log file.
|
||||
for line in open("/tmp/fast.log", "r").readlines():
|
||||
if "[**]" in line:
|
||||
s = line.split("[**]")[1].strip()
|
||||
m = re.search(
|
||||
r"\[\d+\:(?P<sid>\d+)\:(?P<rev>\d+)\] (?P<title>[ -~]+)", s)
|
||||
self.alerts.append({"title": "Suricata rule tiggered: {}".format(m.group('title')),
|
||||
"description": """A network detection rule has been tiggered. It's likely that your device has been compromised
|
||||
or contains a malicious application.""",
|
||||
"level": "High",
|
||||
"id": "SNORT-01"})
|
||||
# Remove fast.log
|
||||
os.remove("/tmp/fast.log")
|
||||
|
||||
def generate_rule_file(self):
|
||||
"""
|
||||
Generate the rules file passed to suricata.
|
||||
:return: bool if operation succeed.
|
||||
"""
|
||||
try:
|
||||
with open(self.rules_file, "w+") as f:
|
||||
f.write("\n".join(self.rules))
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
def generate_contextual_alerts(self):
|
||||
"""
|
||||
Generate contextual alerts related to the current
|
||||
ssid or the device itself.
|
||||
"""
|
||||
apname = get_apname()
|
||||
device = get_device(self.wdir.split("/")[-1])
|
||||
rules = []
|
||||
|
||||
# Devices names to be whitelisted (can appear in UA of HTTP requests. So FP high alerts)
|
||||
device_names = ["iphone", "ipad", "android", "samsung", "galaxy",
|
||||
"huawei", "oneplus", "oppo", "pixel", "xiaomi", "realme", "chrome",
|
||||
"safari"]
|
||||
|
||||
if apname and device:
|
||||
# See if the AP name is sent in clear text over the internet.
|
||||
if len(apname) >= 5:
|
||||
rules.append(
|
||||
'alert tcp {} any -> $EXTERNAL_NET any (content:"{}"; msg:"WiFi name sent in clear text"; sid:10000101; rev:001;)'.format(device["ip_address"], apname))
|
||||
rules.append(
|
||||
'alert udp {} any -> $EXTERNAL_NET any (content:"{}"; msg:"WiFi name sent in clear text"; sid:10000102; rev:001;)'.format(device["ip_address"], apname))
|
||||
|
||||
# See if the device name is sent in clear text over the internet.
|
||||
if len(device["name"]) >= 5 and device["name"].lower() not in device_names:
|
||||
rules.append('alert tcp {} any -> $EXTERNAL_NET any (content:"{}"; msg:"Device name sent in clear text"; sid:10000103; rev:001;)'.format(
|
||||
device["ip_address"], device["name"]))
|
||||
rules.append('alert udp {} any -> $EXTERNAL_NET any (content:"{}"; msg:"Device name sent in clear text"; sid:10000104; rev:001;)'.format(
|
||||
device["ip_address"], device["name"]))
|
||||
|
||||
return rules
|
||||
|
||||
def get_alerts(self):
|
||||
return [dict(t) for t in {tuple(d.items()) for d in self.alerts}]
|
369
analysis/classes/zeekengine.py
Normal file
@ -0,0 +1,369 @@
|
||||
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from classes.parsezeeklogs import ParseZeekLogs
|
||||
from netaddr import IPNetwork, IPAddress
|
||||
from utils import get_iocs, get_config, get_whitelist
|
||||
from ipwhois import IPWhois
|
||||
import subprocess as sp
|
||||
import json
|
||||
import pydig
|
||||
import os
|
||||
|
||||
|
||||
class ZeekEngine(object):
|
||||
|
||||
def __init__(self, capture_directory):
|
||||
self.working_dir = capture_directory
|
||||
self.alerts = []
|
||||
self.conns = []
|
||||
self.ssl = []
|
||||
self.http = []
|
||||
self.dns = []
|
||||
self.files = []
|
||||
|
||||
# Get analysis configuration
|
||||
self.heuristics_analysis = get_config(("analysis", "heuristics"))
|
||||
self.iocs_analysis = get_config(("analysis", "iocs"))
|
||||
self.white_analysis = get_config(("analysis", "whitelist"))
|
||||
|
||||
def fill_dns(self, dir):
|
||||
"""
|
||||
Fill the DNS resolutions thanks to the dns.log.
|
||||
:return: nothing - all resolutions appended to self.dns.
|
||||
"""
|
||||
if os.path.isfile(os.path.join(dir, "dns.log")):
|
||||
for record in ParseZeekLogs(os.path.join(dir, "dns.log"), output_format="json", safe_headers=False):
|
||||
if record is not None:
|
||||
if record["qtype_name"] in ["A", "AAAA"]:
|
||||
d = {"domain": record["query"],
|
||||
"answers": record["answers"].split(",")}
|
||||
if d not in self.dns:
|
||||
self.dns.append(d)
|
||||
|
||||
def netflow_check(self, dir):
|
||||
"""
|
||||
Enrich and check the netflow from the conn.log against whitelist and IOCs.
|
||||
:return: nothing - all stuff appended to self.alerts
|
||||
"""
|
||||
max_ports = get_config(("analysis", "max_ports"))
|
||||
http_default_port = get_config(("analysis", "http_default_port"))
|
||||
|
||||
# Get the netflow from conn.log.
|
||||
if os.path.isfile(os.path.join(dir, "conn.log")):
|
||||
for record in ParseZeekLogs(os.path.join(dir, "conn.log"), output_format="json", safe_headers=False):
|
||||
if record is not None:
|
||||
c = {"ip_dst": record["id.resp_h"],
|
||||
"proto": record["proto"],
|
||||
"port_dst": record["id.resp_p"],
|
||||
"service": record["service"]}
|
||||
if c not in self.conns:
|
||||
self.conns.append(c)
|
||||
|
||||
# Let's add some dns resolutions.
|
||||
for c in self.conns:
|
||||
c["resolution"] = self.resolve(c["ip_dst"])
|
||||
|
||||
# Order the conns list by the resolution field.
|
||||
self.conns = sorted(self.conns, key=lambda c: c["resolution"])
|
||||
|
||||
# Check for whitelisted assets, if any, delete the record.
|
||||
if self.white_analysis:
|
||||
|
||||
wl_cidrs = [IPNetwork(cidr) for cidr in get_whitelist("cidr")]
|
||||
wl_hosts = get_whitelist("ip4addr") + get_whitelist("ip6addr")
|
||||
wl_domains = get_whitelist("domain")
|
||||
|
||||
for i, c in enumerate(self.conns):
|
||||
if c["ip_dst"] in [ip for ip in wl_hosts]:
|
||||
self.conns[i] = False
|
||||
elif c["resolution"] in wl_domains:
|
||||
self.conns[i] = False
|
||||
elif True in [c["resolution"].endswith("." + dom) for dom in wl_domains]:
|
||||
self.conns[i] = False
|
||||
elif True in [IPAddress(c["ip_dst"]) in cidr for cidr in wl_cidrs]:
|
||||
self.conns[i] = False
|
||||
|
||||
# Let's delete whitelisted connections.
|
||||
self.conns = list(filter(lambda c: c != False, self.conns))
|
||||
|
||||
if self.heuristics_analysis:
|
||||
for c in self.conns:
|
||||
# Check for UDP / ICMP (strange from a smartphone.)
|
||||
if c["proto"] in ["UDP", "ICMP"]:
|
||||
self.alerts.append({"title": "{} communication going outside the local network to {}.".format(c["proto"].upper(), c["resolution"]),
|
||||
"description": "The {} protocol is commonly used in internal networks. Please, verify if the host {} leveraged other alerts which may ".format(c["proto"].upper(), c["resolution"])
|
||||
+ "indicates a possible malicious behavior.",
|
||||
"host": c["resolution"],
|
||||
"level": "Moderate",
|
||||
"id": "PROTO-01"})
|
||||
# Check for use of ports over 1024.
|
||||
if c["port_dst"] >= max_ports:
|
||||
self.alerts.append({"title": "{} connection to {} to a port over or equal to {}.".format(c["proto"].upper(), c["resolution"], max_ports),
|
||||
"description": "{} connections have been seen to {} by using the port {}. The use of non-standard port can be sometimes associated to malicious activities. ".format(c["proto"].upper(), c["resolution"], c["port_dst"])
|
||||
+ "We recommend to check if this host has a good reputation by looking on other alerts and search it on the internet.",
|
||||
"host": c["resolution"],
|
||||
"level": "Low",
|
||||
"id": "PROTO-02"})
|
||||
# Check for use of HTTP.
|
||||
if c["service"] == "http" and c["port_dst"] == http_default_port:
|
||||
self.alerts.append({"title": "HTTP communications been done to the host {}".format(c["resolution"]),
|
||||
"description": "Your device exchanged with the host {} by using HTTP, an unencrypted protocol. ".format(c["resolution"])
|
||||
+ "Even if this behavior is not malicious by itself, it is unusual to see HTTP communications issued from smartphone applications "
|
||||
+ "running in the background. Please check the host reputation by searching it on the internet.",
|
||||
"host": c["resolution"],
|
||||
"level": "Low",
|
||||
"id": "PROTO-03"})
|
||||
|
||||
# Check for use of HTTP on a non standard port.
|
||||
if c["service"] == "http" and c["port_dst"] != http_default_port:
|
||||
self.alerts.append({"title": "HTTP communications have been seen to the host {} on a non standard port ({}).".format(c["resolution"], c["port_dst"]),
|
||||
"description": "Your device exchanged with the host {} by using HTTP, an unencrypted protocol on the port {}. ".format(c["resolution"], c["port_dst"])
|
||||
+ "This behavior is quite unusual. Please check the host reputation by searching it on the internet.",
|
||||
"host": c["resolution"],
|
||||
"level": "Moderate",
|
||||
"id": "PROTO-04"})
|
||||
# Check for non-resolved IP address.
|
||||
if c["service"] == c["resolution"]:
|
||||
self.alerts.append({"title": "The server {} hasn't been resolved by any DNS query during the session".format(c["ip_dst"]),
|
||||
"description": "It means that the server {} is likely not resolved by any domain name or the resolution has already been cached by ".format(c["ip_dst"])
|
||||
+ "the device. If the host appears in other alerts, please check it.",
|
||||
"host": c["ip_dst"],
|
||||
"level": "Low",
|
||||
"id": "PROTO-05"})
|
||||
|
||||
if self.iocs_analysis:
|
||||
|
||||
bl_cidrs = [[IPNetwork(cidr[0]), cidr[1]]
|
||||
for cidr in get_iocs("cidr")]
|
||||
bl_hosts = get_iocs("ip4addr") + get_iocs("ip6addr")
|
||||
bl_domains = get_iocs("domain")
|
||||
bl_freedns = get_iocs("freedns")
|
||||
bl_nameservers = get_iocs("ns")
|
||||
bl_tlds = get_iocs("tld")
|
||||
|
||||
for c in self.conns:
|
||||
# Check for blacklisted IP address.
|
||||
for host in bl_hosts:
|
||||
if c["ip_dst"] == host[0]:
|
||||
self.alerts.append({"title": "A connection has been made to {} ({}) which is tagged as {}.".format(c["resolution"], c["ip_dst"], host[1].upper()),
|
||||
"description": "The host {} has been explicitly blacklisted for malicious activities. Your device is likely compromised ".format(c["ip_dst"])
|
||||
+ "and needs to be investigated more deeply by IT security professionals.",
|
||||
|
||||
"host": c["resolution"],
|
||||
"level": "High",
|
||||
"id": "IOC-01"})
|
||||
break
|
||||
# Check for blacklisted CIDR.
|
||||
for cidr in bl_cidrs:
|
||||
if IPAddress(c["ip_dst"]) in cidr[0]:
|
||||
self.alerts.append({"title": "Communication to {} under the CIDR {} which is tagged as {}.".format(c["resolution"], cidr[0], cidr[1].upper()),
|
||||
"description": "The server {} is hosted under a network which is known to host malicious activities. Even if this behavior is not malicious by itself, ".format(c["resolution"])
|
||||
+ "you need to check if other alerts are also mentioning this host. If you have some doubts, please "
|
||||
+ "search this host on the internet to see if its legit or not.",
|
||||
"host": c["resolution"],
|
||||
"level": "Moderate",
|
||||
"id": "IOC-02"})
|
||||
# Check for blacklisted domain.
|
||||
for domain in bl_domains:
|
||||
if c["resolution"].endswith(domain[0]):
|
||||
if domain[1] != "tracker":
|
||||
self.alerts.append({"title": "A DNS request have been done to {} which is tagged as {}.".format(c["resolution"], domain[1].upper()),
|
||||
"description": "The domain name {} seen in the capture has been explicitly tagged as malicious. This indicates that ".format(c["resolution"])
|
||||
+ "your device is likely compromised and needs to be investigated deeply.",
|
||||
"host": c["resolution"],
|
||||
"level": "High",
|
||||
"id": "IOC-03"})
|
||||
else:
|
||||
self.alerts.append({"title": "A DNS request have been done to {} which is tagged as {}.".format(c["resolution"], domain[1].upper()),
|
||||
"description": "The domain name {} seen in the capture has been explicitly tagged as a Tracker. This ".format(c["resolution"])
|
||||
+ "indicates that one of the active apps is geo-tracking your moves.",
|
||||
"host": c["resolution"],
|
||||
"level": "Moderate",
|
||||
"id": "IOC-03"})
|
||||
# Check for blacklisted FreeDNS.
|
||||
for domain in bl_freedns:
|
||||
if c["resolution"].endswith("." + domain[0]):
|
||||
self.alerts.append({"title": "A DNS request have been done to the domain {} which is a Free DNS.".format(c["resolution"]),
|
||||
"description": "The domain name {} is using a Free DNS service. This kind of service is commonly used by cybercriminals ".format(c["resolution"])
|
||||
+ "or state-sponsored threat actors during their operations.",
|
||||
"host": c["resolution"],
|
||||
"level": "Moderate",
|
||||
"id": "IOC-04"})
|
||||
|
||||
# Check for suspect tlds.
|
||||
for tld in bl_tlds:
|
||||
if c["resolution"].endswith(tld[0]):
|
||||
self.alerts.append({"title": "A DNS request have been done to the domain {} which contains a suspect TLD.".format(c["resolution"]),
|
||||
"description": "The domain name {} is using a suspect Top Level Domain ({}). Even not malicious, this non-generic TLD is used regularly by cybercrime ".format(c["resolution"], tld[0])
|
||||
+ "or state-sponsored operations. Please check this domain by searching it on an internet search engine. If other alerts are related to this "
|
||||
+ "host, please consider it as very suspicious.",
|
||||
"host": c["resolution"],
|
||||
"level": "Low",
|
||||
"id": "IOC-05"})
|
||||
|
||||
# Check for use of suspect nameservers.
|
||||
try:
|
||||
name_servers = pydig.query(c["resolution"], "NS")
|
||||
except:
|
||||
name_servers = []
|
||||
|
||||
if len(name_servers):
|
||||
for ns in bl_nameservers:
|
||||
if name_servers[0].endswith(".{}.".format(ns[0])):
|
||||
self.alerts.append({"title": "The domain {} is using a suspect nameserver ({}).".format(c["resolution"], name_servers[0]),
|
||||
"description": "The domain name {} is using a nameserver that has been explicitly tagged to be associated to malicious activities. ".format(c["resolution"])
|
||||
+ "Many cybercriminals and state-sponsored threat actors are using this kind of registrars because they allow cryptocurrencies and anonymous payments.",
|
||||
"host": c["resolution"],
|
||||
"level": "Moderate",
|
||||
"id": "IOC-06"})
|
||||
|
||||
def files_check(self, dir):
|
||||
"""
|
||||
Check on the files.log:
|
||||
* Check certificates sha1
|
||||
* [todo] Check possible binary data or APKs?
|
||||
:return: nothing - all stuff appended to self.alerts
|
||||
"""
|
||||
|
||||
if not self.iocs_analysis:
|
||||
return
|
||||
|
||||
bl_certs = get_iocs("sha1cert")
|
||||
|
||||
if os.path.isfile(os.path.join(dir, "files.log")):
|
||||
for record in ParseZeekLogs(os.path.join(dir, "files.log"), output_format="json", safe_headers=False):
|
||||
if record is not None:
|
||||
f = {"filename": record["filename"],
|
||||
"ip_src": record["tx_hosts"],
|
||||
"ip_dst": record["rx_hosts"],
|
||||
"mime_type": record["mime_type"],
|
||||
"sha1": record["sha1"]}
|
||||
if f not in self.files:
|
||||
self.files.append(f)
|
||||
|
||||
for f in self.files:
|
||||
if f["mime_type"] == "application/x-x509-ca-cert":
|
||||
for cert in bl_certs: # Check for blacklisted certificate.
|
||||
if f["sha1"] == cert[0]:
|
||||
host = self.resolve(f["ip_dst"])
|
||||
self.alerts.append({"title": "A certificate associated to {} activities have been found in the communication to {}.".format(cert[1].upper(), host),
|
||||
"description": "The certificate ({}) associated to {} has been explicitly tagged as malicious. This indicates that ".format(f["sha1"], host)
|
||||
+ "your device is likely compromised and need a forensic analysis.",
|
||||
"host": host,
|
||||
"level": "High",
|
||||
"id": "IOC-07"})
|
||||
|
||||
def ssl_check(self, dir):
|
||||
"""
|
||||
Check on the ssl.log:
|
||||
* SSL connections which doesn't use the 443.
|
||||
* "Free" certificate issuer (taken from the config).
|
||||
* Self-signed certificates.
|
||||
:return: nothing - all stuff appended to self.alerts
|
||||
"""
|
||||
ssl_default_ports = get_config(("analysis", "ssl_default_ports"))
|
||||
free_issuers = get_config(("analysis", "free_issuers"))
|
||||
|
||||
if os.path.isfile(os.path.join(dir, "ssl.log")):
|
||||
for record in ParseZeekLogs(os.path.join(dir, "ssl.log"), output_format="json", safe_headers=False):
|
||||
if record is not None:
|
||||
c = {"host": record['id.resp_h'],
|
||||
"port": record['id.resp_p'],
|
||||
"issuer": record["issuer"],
|
||||
"validation_status": record["validation_status"]}
|
||||
if c not in self.ssl:
|
||||
self.ssl.append(c)
|
||||
|
||||
if self.heuristics_analysis:
|
||||
for cert in self.ssl:
|
||||
host = self.resolve(cert["host"])
|
||||
|
||||
# If the associated host has not whitelisted, check the cert.
|
||||
if host in [c["resolution"] for c in self.conns]:
|
||||
|
||||
# Check for non generic SSL port.
|
||||
if cert["port"] not in ssl_default_ports:
|
||||
self.alerts.append({"title": "SSL connection done on an non standart port ({}) to {}".format(cert["port"], host),
|
||||
"description": "It is not common to see SSL connections issued from smartphones using non-standard ports. Even this can be totally legit,"
|
||||
+ " we recommend to check the reputation of {}, by looking at its WHOIS record, the associated autonomus system, its creation date, and ".format(host)
|
||||
+ " by searching it the internet.",
|
||||
"host": host,
|
||||
"level": "Moderate",
|
||||
"id": "SSL-01"})
|
||||
# Check Free SSL certificates.
|
||||
if cert["issuer"] in free_issuers:
|
||||
self.alerts.append({"title": "An SSL connection to {} is using a free certificate.".format(host),
|
||||
"description": "Free certificates — such as Let's Encrypt — are wildly used by command and control servers associated to "
|
||||
+ "malicious implants or phishing web pages. We recommend to check the host associated to this certificate, "
|
||||
+ "by looking at the domain name, its creation date, or by checking its reputation on the internet.",
|
||||
"host": host,
|
||||
"level": "Moderate",
|
||||
"id": "SSL-02"})
|
||||
# Check for self-signed certificates.
|
||||
if cert["validation_status"] == "self signed certificate in certificate chain":
|
||||
self.alerts.append({"title": "The certificate associated to {} is self-signed.".format(host),
|
||||
"description": "The use of self-signed certificates is a common thing for attacker infrastructure. We recommend to check the host {} ".format(host)
|
||||
+ "which is associated to this certificate, by looking at the domain name (if any), its WHOIS record, its creation date, and "
|
||||
+ " by checking its reputation on the internet.",
|
||||
"host": host,
|
||||
"level": "Moderate",
|
||||
"id": "SSL-03"})
|
||||
|
||||
def alerts_check(self):
|
||||
"""
|
||||
Leverage an advice to the user based on the trigered hosts
|
||||
:return: nothing - all generated alerts appended to self.alerts
|
||||
"""
|
||||
hosts = {}
|
||||
|
||||
for alert in [dict(t) for t in {tuple(d.items()) for d in self.alerts}]:
|
||||
if alert["host"] not in hosts:
|
||||
hosts[alert["host"]] = 1
|
||||
else:
|
||||
hosts[alert["host"]] += 1
|
||||
|
||||
for host, nb in hosts.items():
|
||||
if nb >= get_config(("analysis", "max_alerts")):
|
||||
self.alerts.append({"title": "Check alerts for {}".format(host),
|
||||
"description": "Please, check the reputation of the host {}, this one seems to be malicious as it leveraged {} alerts during the session.".format(host, nb),
|
||||
"host": host,
|
||||
"level": "High",
|
||||
"id": "ADV-01"})
|
||||
|
||||
def resolve(self, ip_addr):
|
||||
"""
|
||||
A simple method to retreive DNS names from IP addresses
|
||||
in order to replace them in alerts.
|
||||
|
||||
:return: String - DNS record or IP Address.
|
||||
"""
|
||||
for record in self.dns:
|
||||
if ip_addr in record["answers"]:
|
||||
return record["domain"]
|
||||
return ip_addr
|
||||
|
||||
def start_zeek(self):
|
||||
"""
|
||||
Start zeek and check the logs.
|
||||
"""
|
||||
sp.Popen("cd {} && /opt/zeek/bin/zeek -Cr capture.pcap protocols/ssl/validate-certs".format(
|
||||
self.working_dir), shell=True).wait()
|
||||
sp.Popen("cd {} && mkdir assets".format(
|
||||
self.working_dir), shell=True).wait()
|
||||
sp.Popen("cd {} && mv *.log assets/".format(self.working_dir),
|
||||
shell=True).wait()
|
||||
|
||||
self.fill_dns(self.working_dir + "/assets/")
|
||||
self.netflow_check(self.working_dir + "/assets/")
|
||||
self.ssl_check(self.working_dir + "/assets/")
|
||||
self.files_check(self.working_dir + "/assets/")
|
||||
self.alerts_check()
|
||||
|
||||
def get_alerts(self):
|
||||
"""
|
||||
Retreive alerts.
|
||||
:return: list - a list of alerts wihout duplicates.
|
||||
"""
|
||||
return [dict(t) for t in {tuple(d.items()) for d in self.alerts}]
|
74
analysis/utils.py
Normal file
@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import sqlite3
|
||||
import datetime
|
||||
import yaml
|
||||
import sys
|
||||
import json
|
||||
import os
|
||||
from functools import reduce
|
||||
|
||||
# I'm not going to use an ORM for that.
|
||||
parent = "/".join(sys.path[0].split("/")[:-1])
|
||||
conn = sqlite3.connect(os.path.join(parent, "tinycheck.sqlite3"))
|
||||
cursor = conn.cursor()
|
||||
|
||||
|
||||
def get_iocs(ioc_type):
|
||||
"""
|
||||
Get a list of IOCs specified by their type.
|
||||
:return: list of IOCs
|
||||
"""
|
||||
cursor.execute(
|
||||
"SELECT value, tag FROM iocs WHERE type = ? ORDER BY value", (ioc_type,))
|
||||
res = cursor.fetchall()
|
||||
return [[r[0], r[1]] for r in res] if res is not None else []
|
||||
|
||||
|
||||
def get_whitelist(elem_type):
|
||||
"""
|
||||
Get a list of whitelisted elements specified by their type.
|
||||
:return: list of elements
|
||||
"""
|
||||
cursor.execute(
|
||||
"SELECT element FROM whitelist WHERE type = ? ORDER BY element", (elem_type,))
|
||||
res = cursor.fetchall()
|
||||
return [r[0] for r in res] if res is not None else []
|
||||
|
||||
|
||||
def get_config(path):
|
||||
"""
|
||||
Read a value from the configuration
|
||||
:return: value (it can be any type)
|
||||
"""
|
||||
config = yaml.load(open(os.path.join(parent, "config.yaml"),
|
||||
"r"), Loader=yaml.SafeLoader)
|
||||
return reduce(dict.get, path, config)
|
||||
|
||||
|
||||
def get_device(token):
|
||||
"""
|
||||
Read the device configuration from device.json file.
|
||||
:return: dict - the device configuration
|
||||
"""
|
||||
try:
|
||||
with open("/tmp/{}/device.json".format(token), "r") as f:
|
||||
return json.load(f)
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
def get_apname():
|
||||
"""
|
||||
Read the current name of the Access Point from
|
||||
the hostapd configuration file
|
||||
:return: str - the AP name
|
||||
"""
|
||||
try:
|
||||
with open("/tmp/hostapd.conf", "r") as f:
|
||||
for l in f.readlines():
|
||||
if "ssid=" in l:
|
||||
return l.replace("ssid=", "").strip()
|
||||
except:
|
||||
pass
|
107
app/.gitignore
vendored
Normal file
@ -0,0 +1,107 @@
|
||||
# Build
|
||||
dist/
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# TypeScript v1 declaration files
|
||||
typings/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
.env.test
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and *not* Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
24
app/backend/README.md
Normal file
@ -0,0 +1,24 @@
|
||||
# tinycheck-backend
|
||||
|
||||
## Project setup
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
### Compiles and hot-reloads for development
|
||||
```
|
||||
npm run serve
|
||||
```
|
||||
|
||||
### Compiles and minifies for production
|
||||
```
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Lints and fixes files
|
||||
```
|
||||
npm run lint
|
||||
```
|
||||
|
||||
### Customize configuration
|
||||
See [Configuration Reference](https://cli.vuejs.org/config/).
|
5
app/backend/babel.config.js
Normal file
@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
'@vue/cli-plugin-babel/preset'
|
||||
]
|
||||
}
|
12010
app/backend/package-lock.json
generated
Normal file
44
app/backend/package.json
Normal file
@ -0,0 +1,44 @@
|
||||
{
|
||||
"name": "tinycheck-backend",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve --copy --port=4201",
|
||||
"build": "vue-cli-service build",
|
||||
"lint": "vue-cli-service lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^0.20.0",
|
||||
"core-js": "^3.6.5",
|
||||
"vue": "^2.6.11",
|
||||
"vue-router": "^3.4.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-plugin-babel": "~4.5.0",
|
||||
"@vue/cli-plugin-eslint": "~4.5.0",
|
||||
"@vue/cli-service": "~4.5.0",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"eslint": "^6.7.2",
|
||||
"eslint-plugin-vue": "^6.2.2",
|
||||
"vue-template-compiler": "^2.6.11"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
"env": {
|
||||
"node": true
|
||||
},
|
||||
"extends": [
|
||||
"plugin:vue/essential",
|
||||
"eslint:recommended"
|
||||
],
|
||||
"parserOptions": {
|
||||
"parser": "babel-eslint"
|
||||
},
|
||||
"rules": {}
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"last 2 versions",
|
||||
"not dead"
|
||||
]
|
||||
}
|
BIN
app/backend/public/favicon.ico
Normal file
After Width: | Height: | Size: 4.2 KiB |
17
app/backend/public/index.html
Normal file
@ -0,0 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
</html>
|
98
app/backend/src/App.vue
Normal file
@ -0,0 +1,98 @@
|
||||
<template>
|
||||
<div class="backend-container off-canvas off-canvas-sidebar-show">
|
||||
<div class="backend-navbar">
|
||||
<a class="off-canvas-toggle btn btn-link btn-action" href="#sidebar">
|
||||
<i class="icon icon-menu"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div class="backend-sidebar off-canvas-sidebar" id="sidebar">
|
||||
<div class="backend-brand">
|
||||
<h2 @click="$router.push('/')" class="title">TinyCheck</h2>
|
||||
</div>
|
||||
<div class="backend-nav">
|
||||
<div class="accordion-container">
|
||||
<div class="accordion">
|
||||
<input id="accordion-configuration" type="checkbox" name="backend-accordion-checkbox" hidden="">
|
||||
<label class="accordion-header c-hand" for="accordion-configuration">Manage Device</label>
|
||||
<div class="accordion-body">
|
||||
<ul class="menu menu-nav">
|
||||
<li class="menu-item">
|
||||
<span @click="$router.push('/device/configuration')">Device config</span>
|
||||
</li>
|
||||
<li class="menu-item">
|
||||
<span @click="$router.push('/device/network')">Network config</span>
|
||||
</li>
|
||||
<li class="menu-item">
|
||||
<span @click="$router.push('/device/db')">Manage database</span>
|
||||
</li>
|
||||
<!-- <li class="menu-item">
|
||||
<span @click="$router.push('/device/user')">User configuration</a>
|
||||
</li> -->
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="accordion">
|
||||
<input id="accordion-iocs" type="checkbox" name="backend-accordion-checkbox" hidden="">
|
||||
<label class="accordion-header c-hand" for="accordion-iocs">Manage IOCs</label>
|
||||
<div class="accordion-body">
|
||||
<ul class="menu menu-nav">
|
||||
<li class="menu-item">
|
||||
<span @click="$router.push('/iocs/manage')">Manage IOCs</span>
|
||||
</li>
|
||||
<li class="menu-item">
|
||||
<span @click="$router.push('/iocs/search')">Search IOCs</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="accordion">
|
||||
<input id="accordion-whitelist" type="checkbox" name="backend-accordion-checkbox" hidden=""/>
|
||||
<label class="accordion-header c-hand" for="accordion-whitelist">Manage Whitelist</label>
|
||||
<div class="accordion-body">
|
||||
<ul class="menu menu-nav">
|
||||
<li class="menu-item">
|
||||
<span @click="$router.push('/whitelist/manage')">Manage elements</span>
|
||||
</li>
|
||||
<li class="menu-item">
|
||||
<span @click="$router.push('/whitelist/search')">Search elements</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="shortcuts">
|
||||
<a href="https://github.com/KasperskyLab/tinycheck"><img src="@/assets/github.png" class="shortcut" /></a>
|
||||
<a href="https://twitter.com/tinycheck"><img src="@/assets/twitter.png" class="shortcut" /></a>
|
||||
</div>
|
||||
</div>
|
||||
<a class="off-canvas-overlay" href="#close"></a>
|
||||
<div class="off-canvas-content">
|
||||
<transition name="fade" mode="out-in">
|
||||
<router-view/>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
document.title = 'TinyCheck Backend'
|
||||
</script>
|
||||
<style>
|
||||
@import './assets/spectre.min.css';
|
||||
@import './assets/spectre-exp.min.css';
|
||||
@import './assets/spectre-icons.min.css';
|
||||
@import './assets/custom.css';
|
||||
/* Face style for router stuff. */
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition-duration: 0.3s;
|
||||
transition-property: opacity;
|
||||
transition-timing-function: ease;
|
||||
}
|
||||
|
||||
.fade-enter,
|
||||
.fade-leave-active {
|
||||
opacity: 0
|
||||
}
|
||||
</style>
|
653
app/backend/src/assets/custom.css
Normal file
@ -0,0 +1,653 @@
|
||||
/*
|
||||
This CSS was forked from the awsome Spectre.css docs.
|
||||
Spectre.css Docs | MIT License | github.com/picturepan2/spectre
|
||||
*/
|
||||
|
||||
|
||||
.off-canvas .off-canvas-toggle {
|
||||
font-size: 1rem;
|
||||
left: 1.5rem;
|
||||
position: fixed;
|
||||
top: 1rem
|
||||
}
|
||||
|
||||
.off-canvas .off-canvas-sidebar {
|
||||
width: 12rem
|
||||
}
|
||||
|
||||
.off-canvas .off-canvas-content {
|
||||
padding: 0
|
||||
}
|
||||
|
||||
.backend-container {
|
||||
min-height: 100vh
|
||||
}
|
||||
|
||||
.backend-navbar {
|
||||
height: 3.8rem;
|
||||
position: fixed;
|
||||
right: 0;
|
||||
top: 0;
|
||||
z-index: 100
|
||||
}
|
||||
|
||||
.backend-navbar .btns {
|
||||
position: absolute;
|
||||
right: 1.5rem;
|
||||
top: 1rem;
|
||||
width: 14rem
|
||||
}
|
||||
|
||||
.backend-navbar .algolia-autocomplete {
|
||||
-ms-flex: 1 1 auto;
|
||||
flex: 1 1 auto
|
||||
}
|
||||
|
||||
.backend-sidebar .backend-nav {
|
||||
bottom: 1.5rem;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
overflow-y: auto;
|
||||
padding: .5rem 1.5rem;
|
||||
position: fixed;
|
||||
top: 3.5rem;
|
||||
width: 12rem
|
||||
}
|
||||
|
||||
.backend-sidebar .accordion {
|
||||
margin-bottom: .75rem
|
||||
}
|
||||
|
||||
.backend-sidebar .accordion input~.accordion-header {
|
||||
color: #455060;
|
||||
font-size: .65rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase
|
||||
}
|
||||
|
||||
.backend-sidebar .accordion input:checked~.accordion-header {
|
||||
color: #505c6e
|
||||
}
|
||||
|
||||
.backend-sidebar .accordion .menu .menu-item {
|
||||
font-size: .7rem;
|
||||
padding-left: 1rem;
|
||||
cursor:pointer;
|
||||
}
|
||||
|
||||
.backend-sidebar .accordion .menu .menu-item>a {
|
||||
background: 0 0;
|
||||
color: #66758c;
|
||||
display: inline-block
|
||||
}
|
||||
|
||||
.backend-content {
|
||||
-ms-flex: 1 1 auto;
|
||||
flex: 1 1 auto;
|
||||
padding: 0 4rem;
|
||||
width: calc(100vw - 12rem)
|
||||
}
|
||||
|
||||
.backend-content>.container {
|
||||
margin-left: 0;
|
||||
max-width: 800px;
|
||||
padding-bottom: 1.5rem
|
||||
}
|
||||
|
||||
.backend-content .anchor {
|
||||
color: #6362dc;
|
||||
display: none;
|
||||
margin-left: .2rem;
|
||||
padding: 0 .2rem
|
||||
}
|
||||
|
||||
.backend-content .anchor:focus,
|
||||
.backend-content .anchor:hover {
|
||||
display: inline;
|
||||
text-decoration: none
|
||||
}
|
||||
|
||||
.backend-content .s-subtitle,
|
||||
.backend-content .s-title {
|
||||
line-height: 1.8rem;
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 1rem;
|
||||
padding-top: 1rem;
|
||||
position: static
|
||||
}
|
||||
|
||||
@supports ((position:-webkit-sticky) or (position:sticky)) {
|
||||
.backend-content .s-subtitle,
|
||||
.backend-content .s-title {
|
||||
position: -webkit-sticky;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 99
|
||||
}
|
||||
.backend-content .s-subtitle::before,
|
||||
.backend-content .s-title::before {
|
||||
background: #fff;
|
||||
bottom: 0;
|
||||
content: "";
|
||||
display: block;
|
||||
left: -10px;
|
||||
position: absolute;
|
||||
right: -10px;
|
||||
top: -5px;
|
||||
z-index: -1
|
||||
}
|
||||
}
|
||||
|
||||
.backend-content .s-subtitle:hover .anchor,
|
||||
.backend-content .s-title:hover .anchor {
|
||||
display: inline
|
||||
}
|
||||
|
||||
.backend-content .s-subtitle+.backend-note,
|
||||
.backend-content .s-title+.backend-note {
|
||||
margin-top: .4rem
|
||||
}
|
||||
|
||||
.backend-content .backend-demo {
|
||||
padding-bottom: 1rem;
|
||||
padding-top: 1rem
|
||||
}
|
||||
|
||||
.backend-content .backend-demo .card {
|
||||
border: 0;
|
||||
box-shadow: 0 .25rem 1rem rgba(48, 55, 66, .15);
|
||||
height: 100%
|
||||
}
|
||||
|
||||
.backend-content .column {
|
||||
padding: .4rem
|
||||
}
|
||||
|
||||
.backend-content .backend-block {
|
||||
border-radius: .1rem;
|
||||
padding: .4rem
|
||||
}
|
||||
|
||||
.backend-content .backend-block.bg-gray {
|
||||
background: #eef0f3
|
||||
}
|
||||
|
||||
.backend-content .backend-shape {
|
||||
height: 4.8rem;
|
||||
line-height: 1.2rem;
|
||||
padding: 1.8rem 0;
|
||||
width: 4.8rem
|
||||
}
|
||||
|
||||
.backend-content .backend-dot {
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
height: .5rem;
|
||||
padding: 0;
|
||||
width: .5rem
|
||||
}
|
||||
|
||||
.backend-content .backend-table td,
|
||||
.backend-content .backend-table th {
|
||||
padding: .75rem .25rem
|
||||
}
|
||||
|
||||
.backend-content .backend-color {
|
||||
border-radius: .1rem;
|
||||
margin: .25rem 0;
|
||||
padding: 5rem .5rem .5rem
|
||||
}
|
||||
|
||||
.backend-content .backend-color .color-subtitle {
|
||||
font-size: .7rem;
|
||||
opacity: .75
|
||||
}
|
||||
|
||||
.backend-content .code .hljs-tag {
|
||||
color: #505c6e
|
||||
}
|
||||
|
||||
.backend-content .code .hljs-comment {
|
||||
color: #bcc3ce
|
||||
}
|
||||
|
||||
.backend-content .code .hljs-class,
|
||||
.backend-content .code .hljs-number,
|
||||
.backend-content .code .hljs-string,
|
||||
.backend-content .code .hljs-title {
|
||||
color: #5755d9
|
||||
}
|
||||
|
||||
.backend-content .code .hljs-attribute,
|
||||
.backend-content .code .hljs-built_in,
|
||||
.backend-content .code .hljs-keyword,
|
||||
.backend-content .code .hljs-name,
|
||||
.backend-content .code .hljs-variable {
|
||||
color: #d73e48
|
||||
}
|
||||
|
||||
.backend-content .code .hljs-hexcolor,
|
||||
.backend-content .code .hljs-value {
|
||||
color: #505c6e
|
||||
}
|
||||
|
||||
.backend-content .c-select-all {
|
||||
-webkit-user-select: all;
|
||||
-moz-user-select: all;
|
||||
-ms-user-select: all;
|
||||
user-select: all
|
||||
}
|
||||
|
||||
.backend-content .panel {
|
||||
height: 75vh
|
||||
}
|
||||
|
||||
.backend-content .panel .tile {
|
||||
margin: .75rem 0
|
||||
}
|
||||
|
||||
.backend-content .parallax {
|
||||
margin: 2rem auto
|
||||
}
|
||||
|
||||
.backend-content .form-autocomplete .menu {
|
||||
position: static
|
||||
}
|
||||
|
||||
.backend-content .example-tile-icon {
|
||||
align-content: space-around;
|
||||
align-items: center;
|
||||
background: #5755d9;
|
||||
border-radius: .1rem;
|
||||
color: #fff;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-ms-flex-align: center;
|
||||
-ms-flex-line-pack: distribute;
|
||||
font-size: 1.2rem;
|
||||
height: 2rem;
|
||||
width: 2rem
|
||||
}
|
||||
|
||||
.backend-content .example-tile-icon .icon {
|
||||
margin: auto
|
||||
}
|
||||
|
||||
.backend-content .comparison-slider {
|
||||
height: auto;
|
||||
padding-bottom: 56.2222%
|
||||
}
|
||||
|
||||
.backend-content .comparison-slider .filter-grayscale {
|
||||
filter: grayscale(75%)
|
||||
}
|
||||
|
||||
.backend-content .off-canvas {
|
||||
position: relative
|
||||
}
|
||||
|
||||
.backend-content .off-canvas .off-canvas-toggle {
|
||||
left: .4rem;
|
||||
position: absolute;
|
||||
top: .4rem;
|
||||
z-index: 1
|
||||
}
|
||||
|
||||
.backend-brand {
|
||||
color: #CCC;
|
||||
height: 2rem;
|
||||
left: 1.5rem;
|
||||
position: fixed;
|
||||
top: .85rem
|
||||
}
|
||||
|
||||
.backend-brand .backend-logo {
|
||||
align-items: center;
|
||||
border-radius: .1rem;
|
||||
display: -ms-inline-flexbox;
|
||||
display: inline-flex;
|
||||
-ms-flex-align: center;
|
||||
font-size: .7rem;
|
||||
height: 2rem;
|
||||
padding: .2rem;
|
||||
width: auto
|
||||
}
|
||||
|
||||
.backend-brand .backend-logo:focus,
|
||||
.backend-brand .backend-logo:hover {
|
||||
text-decoration: none
|
||||
}
|
||||
|
||||
.backend-brand .backend-logo img {
|
||||
display: inline-block;
|
||||
height: auto;
|
||||
width: 1.6rem
|
||||
}
|
||||
|
||||
.backend-brand .backend-logo h2 {
|
||||
display: inline-block;
|
||||
font-size: .8rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.5rem;
|
||||
margin-bottom: 0;
|
||||
margin-left: .5rem;
|
||||
margin-right: .3rem
|
||||
}
|
||||
|
||||
.backend-footer {
|
||||
color: #bcc3ce;
|
||||
padding: .5rem
|
||||
}
|
||||
|
||||
.backend-footer a {
|
||||
color: #66758c
|
||||
}
|
||||
|
||||
@media (max-width:960px) {
|
||||
.off-canvas .off-canvas-toggle {
|
||||
z-index: 300
|
||||
}
|
||||
.off-canvas .off-canvas-content {
|
||||
width: 100%
|
||||
}
|
||||
.backend-sidebar .backend-brand {
|
||||
margin: .85rem 1.5rem;
|
||||
padding: 0;
|
||||
position: static
|
||||
}
|
||||
.backend-sidebar .backend-nav {
|
||||
margin-top: 1rem;
|
||||
position: static
|
||||
}
|
||||
.backend-sidebar .menu .menu-item>a {
|
||||
padding: .3rem .4rem
|
||||
}
|
||||
.backend-navbar {
|
||||
-webkit-backdrop-filter: blur(5px);
|
||||
backdrop-filter: blur(5px);
|
||||
background: rgba(247, 248, 249, .65);
|
||||
left: 0
|
||||
}
|
||||
.backend-content {
|
||||
min-width: auto;
|
||||
padding: 0 1.5rem;
|
||||
width: 100%
|
||||
}
|
||||
.backend-content .s-subtitle,
|
||||
.backend-content .s-title {
|
||||
padding-top: 5rem;
|
||||
position: static
|
||||
}
|
||||
.backend-content .s-subtitle::before,
|
||||
.backend-content .s-title::before {
|
||||
content: none
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width:600px) {
|
||||
.off-canvas .off-canvas-toggle {
|
||||
left: .5rem
|
||||
}
|
||||
.backend-navbar .btns {
|
||||
right: .9rem
|
||||
}
|
||||
.backend-sidebar .backend-brand {
|
||||
margin: .85rem 1rem
|
||||
}
|
||||
.backend-sidebar .backend-nav {
|
||||
padding: .5rem 1rem
|
||||
}
|
||||
.backend-content {
|
||||
padding: 0 .5rem
|
||||
}
|
||||
.backend-content .backend-block {
|
||||
padding: .4rem .1rem
|
||||
}
|
||||
}
|
||||
|
||||
/* Ok, here the CSS specific to TinyCheck */
|
||||
|
||||
@font-face {
|
||||
font-family: 'Lobster';
|
||||
font-weight: normal;
|
||||
src: url('fonts/lobster.ttf') format('truetype');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Roboto-Bold";
|
||||
src: url("fonts/Roboto-Bold.eot"); /* IE9 Compat Modes */
|
||||
src: url("fonts/Roboto-Bold.eot?#iefix") format("embedded-opentype"), /* IE6-IE8 */
|
||||
url("fonts/Roboto-Bold.otf") format("opentype"), /* Open Type Font */
|
||||
url("fonts/Roboto-Bold.svg") format("svg"), /* Legacy iOS */
|
||||
url("fonts/Roboto-Bold.ttf") format("truetype"), /* Safari, Android, iOS */
|
||||
url("fonts/Roboto-Bold.woff") format("woff"), /* Modern Browsers */
|
||||
url("fonts/Roboto-Bold.woff2") format("woff2"); /* Modern Browsers */
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Roboto-Regular";
|
||||
src: url("fonts/Roboto-Regular.eot"); /* IE9 Compat Modes */
|
||||
src: url("fonts/Roboto-Regular.eot?#iefix") format("embedded-opentype"), /* IE6-IE8 */
|
||||
url("fonts/Roboto-Regular.otf") format("opentype"), /* Open Type Font */
|
||||
url("fonts/Roboto-Regular.svg") format("svg"), /* Legacy iOS */
|
||||
url("fonts/Roboto-Regular.ttf") format("truetype"), /* Safari, Android, iOS */
|
||||
url("fonts/Roboto-Regular.woff") format("woff"), /* Modern Browsers */
|
||||
url("fonts/Roboto-Regular.woff2") format("woff2"); /* Modern Browsers */
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
h1, h2, h3 {
|
||||
font-family: Lobster;
|
||||
color: #484848;
|
||||
}
|
||||
|
||||
h4, h5 {
|
||||
font-family: "Roboto-Bold";
|
||||
color: #484848;
|
||||
}
|
||||
|
||||
.btn {
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.btn:focus, .btn:hover {
|
||||
background: #494949;
|
||||
border-color: #494949;
|
||||
text-decoration: none;
|
||||
color: #DBDBDB;
|
||||
}
|
||||
|
||||
.px150 {
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.width-full {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tab-block {
|
||||
margin-bottom: 35px;
|
||||
}
|
||||
|
||||
.toast {
|
||||
margin-top: 5px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.frame-export {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.form-upload {
|
||||
position: relative;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.form-upload .upload-field {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
opacity: 0;
|
||||
z-index:1000;
|
||||
width:100%;
|
||||
height:100%;
|
||||
}
|
||||
|
||||
.tlp-white {
|
||||
font-size: .6rem;
|
||||
height: 1.4rem;
|
||||
padding: .05rem .3rem;
|
||||
background-color: #FFF;
|
||||
line-height: 1.2rem;
|
||||
text-transform: uppercase;
|
||||
font-weight: bold;
|
||||
border: 2px solid #efefef;
|
||||
vertical-align: middle;
|
||||
border-radius: .1rem;
|
||||
}
|
||||
|
||||
.tlp-green {
|
||||
font-size: .6rem;
|
||||
height: 1.4rem;
|
||||
padding: .05rem .3rem;
|
||||
background-color: #199a09cf;
|
||||
line-height: 1.2rem;
|
||||
text-transform: uppercase;
|
||||
font-weight: bold;
|
||||
border: 2px solid #0f8600cf;
|
||||
vertical-align: middle;
|
||||
color:#FFF;
|
||||
border-radius: .1rem;
|
||||
}
|
||||
|
||||
.tlp-amber {
|
||||
font-size: .6rem;
|
||||
height: 1.4rem;
|
||||
padding: .05rem .3rem;
|
||||
background-color: #ffc000cf;
|
||||
line-height: 1.2rem;
|
||||
text-transform: uppercase;
|
||||
font-weight: bold;
|
||||
border: 2px solid #ffc000cf;
|
||||
vertical-align: middle;
|
||||
color:#FFF;
|
||||
border-radius: .1rem;
|
||||
}
|
||||
|
||||
.tlp-red {
|
||||
font-size: .6rem;
|
||||
height: 1.4rem;
|
||||
padding: .05rem .3rem;
|
||||
background-color: #ff0033;
|
||||
line-height: 1.2rem;
|
||||
text-transform: uppercase;
|
||||
font-weight: bold;
|
||||
border: 2px solid #ff0033;
|
||||
vertical-align: middle;
|
||||
color:#FFF;
|
||||
border-radius: .1rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-family: 'Lobster';
|
||||
color: #c5c5c5;
|
||||
cursor: pointer;
|
||||
width: fit-content;
|
||||
height: 2rem;
|
||||
left: 1.5rem;
|
||||
position: fixed;
|
||||
top: .85rem
|
||||
}
|
||||
|
||||
#network-thumbnail {
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.interfaces-container {
|
||||
margin-top:20px;
|
||||
margin-bottom:40px;
|
||||
}
|
||||
|
||||
.interface-label {
|
||||
text-transform: uppercase;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
text-align: center;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
border-color: inherit;
|
||||
-webkit-box-shadow: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
border-color: inherit;
|
||||
-webkit-box-shadow: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.tab .tab-item a:focus {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.shortcuts {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 20000;
|
||||
padding: 20px;
|
||||
opacity: .2;
|
||||
}
|
||||
|
||||
.shortcut {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin-left: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.whitespace {
|
||||
height:100px;
|
||||
display:block;
|
||||
}
|
||||
|
||||
.alert-toaster-visible {
|
||||
position:fixed;
|
||||
right:15px;
|
||||
top:15px;
|
||||
padding:10px;
|
||||
background-color: #484848;
|
||||
border-radius: 5px;
|
||||
color: #ccc;
|
||||
font-size: 12px;
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
transition: opacity .5s linear;
|
||||
}
|
||||
|
||||
.alert-toaster-hidden {
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
transition: visibility 0s .5s, opacity .5s linear;
|
||||
}
|
||||
|
||||
.comment-block {
|
||||
padding: 10px;
|
||||
font-size: 12px;
|
||||
background-color: #FAFAFA;
|
||||
}
|
||||
|
||||
.capi {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.upper {
|
||||
text-transform: uppercase;
|
||||
}
|
BIN
app/backend/src/assets/fonts/Roboto-Bold.eot
Normal file
BIN
app/backend/src/assets/fonts/Roboto-Bold.otf
Normal file
11535
app/backend/src/assets/fonts/Roboto-Bold.svg
Normal file
After Width: | Height: | Size: 805 KiB |
BIN
app/backend/src/assets/fonts/Roboto-Bold.ttf
Normal file
BIN
app/backend/src/assets/fonts/Roboto-Bold.woff
Normal file
BIN
app/backend/src/assets/fonts/Roboto-Bold.woff2
Normal file
BIN
app/backend/src/assets/fonts/Roboto-Regular.eot
Normal file
BIN
app/backend/src/assets/fonts/Roboto-Regular.otf
Normal file
11080
app/backend/src/assets/fonts/Roboto-Regular.svg
Normal file
After Width: | Height: | Size: 784 KiB |
BIN
app/backend/src/assets/fonts/Roboto-Regular.ttf
Normal file
BIN
app/backend/src/assets/fonts/Roboto-Regular.woff
Normal file
BIN
app/backend/src/assets/fonts/Roboto-Regular.woff2
Normal file
BIN
app/backend/src/assets/fonts/lobster.ttf
Normal file
BIN
app/backend/src/assets/github.png
Normal file
After Width: | Height: | Size: 4.2 KiB |
BIN
app/backend/src/assets/network-home.png
Normal file
After Width: | Height: | Size: 315 KiB |
BIN
app/backend/src/assets/network.png
Normal file
After Width: | Height: | Size: 133 KiB |
1
app/backend/src/assets/spectre-exp.min.css
vendored
Normal file
1
app/backend/src/assets/spectre-icons.min.css
vendored
Normal file
1
app/backend/src/assets/spectre.min.css
vendored
Normal file
BIN
app/backend/src/assets/twitter.png
Normal file
After Width: | Height: | Size: 7.8 KiB |
10
app/backend/src/main.js
Normal file
@ -0,0 +1,10 @@
|
||||
import Vue from 'vue'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
Vue.config.productionTip = true
|
||||
Vue.config.devtools = true
|
||||
|
||||
new Vue({
|
||||
router,
|
||||
render: h => h(App)
|
||||
}).$mount('#app')
|
63
app/backend/src/router/index.js
Normal file
@ -0,0 +1,63 @@
|
||||
import Vue from 'vue'
|
||||
import VueRouter from 'vue-router'
|
||||
|
||||
Vue.use(VueRouter)
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'default',
|
||||
component: () => import('../views/home.vue'),
|
||||
props: true
|
||||
},
|
||||
{
|
||||
path: '/device/configuration',
|
||||
name: 'device-configuration',
|
||||
component: () => import('../views/edit-configuration.vue'),
|
||||
props: true
|
||||
},
|
||||
{
|
||||
path: '/device/network',
|
||||
name: 'device-network',
|
||||
component: () => import('../views/network-manage.vue'),
|
||||
props: true
|
||||
},
|
||||
{
|
||||
path: '/device/db',
|
||||
name: 'db-manage',
|
||||
component: () => import('../views/db-manage.vue'),
|
||||
props: true
|
||||
},
|
||||
{
|
||||
path: '/iocs/manage',
|
||||
name: 'iocs-manage',
|
||||
component: () => import('../views/iocs-manage.vue'),
|
||||
props: true
|
||||
},
|
||||
{
|
||||
path: '/iocs/search',
|
||||
name: 'iocs-search',
|
||||
component: () => import('../views/iocs-search.vue'),
|
||||
props: true
|
||||
},
|
||||
{
|
||||
path: '/whitelist/manage',
|
||||
name: 'whitelist-manage',
|
||||
component: () => import('../views/whitelist-manage.vue'),
|
||||
props: true
|
||||
},
|
||||
{
|
||||
path: '/whitelist/search',
|
||||
name: 'whitelist-search',
|
||||
component: () => import('../views/whitelist-search.vue'),
|
||||
props: true
|
||||
}
|
||||
]
|
||||
|
||||
const router = new VueRouter({
|
||||
mode: 'history',
|
||||
base: process.env.BASE_URL,
|
||||
routes
|
||||
})
|
||||
|
||||
export default router
|
75
app/backend/src/views/db-manage.vue
Normal file
@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<div class="backend-content" id="content">
|
||||
<div class="column col-6 col-xs-12">
|
||||
<h3 class="s-title">Manage database</h3>
|
||||
<ul class="tab tab-block">
|
||||
<li class="tab-item">
|
||||
<a href="#" v-on:click="switch_tab('import')" v-bind:class="{ active: tabs.import }">Import database</a>
|
||||
</li>
|
||||
<li class="tab-item">
|
||||
<a href="#" v-on:click="switch_tab('export')" v-bind:class="{ active: tabs.export }">Export database</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div v-if="tabs.export">
|
||||
<iframe :src="export_url" class="frame-export"></iframe>
|
||||
</div>
|
||||
<div v-if="tabs.import">
|
||||
<label class="form-upload empty" for="upload">
|
||||
<input type="file" class="upload-field" id="upload" @change="import_from_file">
|
||||
<p class="empty-title h5">Drop or select a database to import.</p>
|
||||
<p class="empty-subtitle">The database needs to be an export from a TinyCheck instance.</p>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
|
||||
export default {
|
||||
name: 'db-manage',
|
||||
data() {
|
||||
return {
|
||||
tabs: { "import" : true, "export" : false },
|
||||
jwt:""
|
||||
}
|
||||
},
|
||||
props: { },
|
||||
methods: {
|
||||
switch_tab: function(tab) {
|
||||
Object.keys(this.tabs).forEach(key => {
|
||||
if( key == tab ){
|
||||
this.tabs[key] = true
|
||||
} else {
|
||||
this.tabs[key] = false
|
||||
}
|
||||
});
|
||||
},
|
||||
import_from_file: function(ev) {
|
||||
var formData = new FormData();
|
||||
formData.append("file", ev.target.files[0]);
|
||||
axios.post('/api/config/db/import', formData, {
|
||||
headers: {
|
||||
"Content-Type" : "multipart/form-data",
|
||||
"X-Token" : this.jwt
|
||||
}
|
||||
})
|
||||
},
|
||||
async get_jwt(){
|
||||
await axios.get(`/api/get-token`, { timeout: 10000 })
|
||||
.then(response => {
|
||||
if(response.data.token){
|
||||
this.jwt = response.data.token
|
||||
}
|
||||
})
|
||||
.catch(err => (console.log(err)))
|
||||
}
|
||||
},
|
||||
created: function() {
|
||||
this.get_jwt().then(() => {
|
||||
this.export_url = `/api/config/db/export?token=${this.jwt}`
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
202
app/backend/src/views/edit-configuration.vue
Normal file
@ -0,0 +1,202 @@
|
||||
<template>
|
||||
<div class="backend-content" id="content">
|
||||
<div v-bind:class="{ 'alert-toaster-visible' : toaster.show, 'alert-toaster-hidden' : !toaster.show }">{{toaster.message}}</div>
|
||||
<div class="modal active" id="modal-id" v-if="check_certificate">
|
||||
<a href="#close" class="modal-overlay" aria-label="Close"></a>
|
||||
<div class="modal-container">
|
||||
<div class="modal-header">
|
||||
<a href="#close" class="btn btn-clear float-right" aria-label="Close" @click="check_certificate = false"></a>
|
||||
<div class="modal-title h5">Certificate validation</div>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="content">
|
||||
Do you trust this certificate?
|
||||
<pre class="code" data-lang="CERTIFICATE">
|
||||
<code>{{certificate}}</code>
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-primary" @click="validate_server()">Yes I trust it.</button><a class="btn btn-link" href="#modals" @click="no_trust()">No I don't trust it</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column col-6 col-xs-12">
|
||||
<h3 class="s-title">Configuration </h3>
|
||||
<h5 class="s-subtitle">Device configuration</h5>
|
||||
<div class="form-group">
|
||||
<label class="form-switch">
|
||||
<input type="checkbox" @change="switch_config('frontend', 'kiosk_mode')" v-model="config.frontend.kiosk_mode">
|
||||
<i class="form-icon"></i> Use TinyCheck in Kiosk-mode.
|
||||
</label>
|
||||
<label class="form-switch">
|
||||
<input type="checkbox" @change="switch_config('frontend', 'virtual_keyboard')" v-model="config.frontend.virtual_keyboard">
|
||||
<i class="form-icon"></i> Use virtual keyboard (for touch screen)
|
||||
</label>
|
||||
<label class="form-switch">
|
||||
<input type="checkbox" @change="switch_config('frontend', 'hide_mouse')" v-model="config.frontend.hide_mouse">
|
||||
<i class="form-icon"></i> Hide mouse (for touch screen)
|
||||
</label>
|
||||
<label class="form-switch">
|
||||
<input type="checkbox" @change="switch_config('network', 'tokenized_ssids')" v-model="config.network.tokenized_ssids">
|
||||
<i class="form-icon"></i> Use tokenized SSIDs (eg. [ssid-name]-[hex-str]).
|
||||
</label>
|
||||
<label class="form-switch">
|
||||
<input type="checkbox" @change="switch_config('frontend', 'download_links')" v-model="config.frontend.download_links">
|
||||
<i class="form-icon"></i> Use in-browser download for network captures.
|
||||
</label>
|
||||
<label class="form-switch">
|
||||
<input type="checkbox" @change="switch_config('frontend', 'sparklines')" v-model="config.frontend.sparklines">
|
||||
<i class="form-icon"></i> Show background sparklines during the capture.
|
||||
</label>
|
||||
<label class="form-switch">
|
||||
<input type="checkbox" @change="switch_config('frontend', 'remote_access')" v-model="config.frontend.remote_access">
|
||||
<i class="form-icon"></i> Allow remote access to the frontend.
|
||||
</label>
|
||||
<label class="form-switch">
|
||||
<input type="checkbox" @change="switch_config('backend', 'remote_access')" v-model="config.backend.remote_access">
|
||||
<i class="form-icon"></i> Allow remote access to the backend.
|
||||
</label>
|
||||
</div>
|
||||
<h5 class="s-subtitle">Analysis configuration</h5>
|
||||
<div class="form-group">
|
||||
<label class="form-switch">
|
||||
<input type="checkbox" @change="local_analysis('analysis', 'heuristics')" v-model="config.analysis.heuristics">
|
||||
<i class="form-icon"></i> Use heuristic detection for suspect behaviour.
|
||||
</label>
|
||||
<label class="form-switch">
|
||||
<input type="checkbox" @change="local_analysis('analysis', 'iocs')" v-model="config.analysis.iocs">
|
||||
<i class="form-icon"></i> Use Indicator of Compromise (IoC) based detection.
|
||||
</label>
|
||||
<label class="form-switch">
|
||||
<input type="checkbox" @change="local_analysis('analysis', 'whitelist')" v-model="config.analysis.whitelist">
|
||||
<i class="form-icon"></i> Use whitelist to prevent false positives.
|
||||
</label>
|
||||
</div>
|
||||
<h5 class="s-subtitle">User credentials</h5>
|
||||
<div class="form-group">
|
||||
<div class="column col-10 col-xs-12">
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="user-login">User login</label>
|
||||
<div class="input-group">
|
||||
<input class="form-input" id="user-login" type="text" v-model="config.backend.login">
|
||||
<button class="btn btn-primary input-group-btn px150" @click="change_login()">Update it</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="user-login">User password</label>
|
||||
<div class="input-group">
|
||||
<input class="form-input" id="user-login" type="password" placeholder="●●●●●●" v-model="config.backend.password">
|
||||
<button class="btn btn-primary input-group-btn px150" @click="change_password()">Update it</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="whitespace"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
|
||||
export default {
|
||||
name: 'edit-configuration',
|
||||
data() {
|
||||
return {
|
||||
config: {},
|
||||
check_certificate: false,
|
||||
certificate: "",
|
||||
toaster: { show: false, message : "", type : null }
|
||||
}
|
||||
},
|
||||
props: {},
|
||||
methods: {
|
||||
switch_config: function(cat, key) {
|
||||
axios.get(`/api/config/switch/${cat}/${key}`, {
|
||||
timeout: 10000,
|
||||
headers: { 'X-Token': this.jwt }
|
||||
}).then(response => {
|
||||
if (response.data.status) {
|
||||
if (response.data.message == "Key switched to true") {
|
||||
this.toaster = { show : true, message : "Configuration updated", type : "success" }
|
||||
setTimeout(function () { this.toaster = { show : false } }.bind(this), 1000)
|
||||
this.config[cat][key] = true
|
||||
} else if (response.data.message == "Key switched to false") {
|
||||
this.toaster = { show : true, message : "Configuration updated", type : "success" }
|
||||
setTimeout(function () { this.toaster = { show : false } }.bind(this), 1000)
|
||||
this.config[cat][key] = false
|
||||
} else {
|
||||
this.toaster = { show : true, message : "The key doesn't exist", type : "error" }
|
||||
setTimeout(function () { this.toaster = { show : false } }.bind(this), 1000)
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(err => (console.log(err)))
|
||||
},
|
||||
load_config: function() {
|
||||
axios.get(`/api/config/list`, {
|
||||
timeout: 10000,
|
||||
headers: { 'X-Token': this.jwt }
|
||||
}).then(response => {
|
||||
if (response.data) {
|
||||
this.config = response.data
|
||||
this.config.backend.password = ""
|
||||
}
|
||||
})
|
||||
.catch(err => (console.log(err)))
|
||||
},
|
||||
async get_jwt() {
|
||||
await axios.get(`/api/get-token`, { timeout: 10000 })
|
||||
.then(response => {
|
||||
if (response.data.token) {
|
||||
this.jwt = response.data.token
|
||||
}
|
||||
})
|
||||
.catch(err => (console.log(err)))
|
||||
},
|
||||
local_analysis: function(cat, key) {
|
||||
this.switch_config(cat, key);
|
||||
if (this.config.analysis.remote != false)
|
||||
this.switch_config("analysis", "remote");
|
||||
},
|
||||
change_login: function() {
|
||||
axios.get(`/api/config/edit/backend/login/${this.config.backend.login}`, {
|
||||
timeout: 10000,
|
||||
headers: { 'X-Token': this.jwt }
|
||||
}).then(response => {
|
||||
if (response.data.status) {
|
||||
this.toaster = { show : true, message : "Login changed", type : "success" }
|
||||
setTimeout(function () { this.toaster = { show : false } }.bind(this), 1000)
|
||||
} else {
|
||||
this.toaster = { show : true, message : "Login not changed", type : "error" }
|
||||
setTimeout(function () { this.toaster = { show : false } }.bind(this), 1000)
|
||||
}
|
||||
})
|
||||
.catch(err => (console.log(err)))
|
||||
},
|
||||
change_password: function() {
|
||||
axios.get(`/api/config/edit/backend/password/${this.config.backend.password}`, {
|
||||
timeout: 10000,
|
||||
headers: { 'X-Token': this.jwt }
|
||||
}).then(response => {
|
||||
if (response.data.status) {
|
||||
this.toaster = { show : true, message : "Password changed", type : "success" }
|
||||
setTimeout(function () { this.toaster = { show : false } }.bind(this), 1000)
|
||||
} else {
|
||||
this.toaster = { show : true, message : "Password not changed", type : "error" }
|
||||
setTimeout(function () { this.toaster = { show : false } }.bind(this), 1000)
|
||||
}
|
||||
})
|
||||
.catch(err => (console.log(err)))
|
||||
}
|
||||
},
|
||||
created: function() {
|
||||
this.get_jwt().then(() => {
|
||||
this.load_config();
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
19
app/backend/src/views/home.vue
Normal file
@ -0,0 +1,19 @@
|
||||
<template>
|
||||
<div class="backend-content" id="content">
|
||||
<div class="column col-6 col-xs-12">
|
||||
<div class="container">
|
||||
<h3 class="s-title">Getting started with TinyCheck</h3>
|
||||
|
||||
<img src="@/assets/network-home.png" id="network-thumbnail" />
|
||||
|
||||
<p>TinyCheck allows you to capture easily internet communications from a smartphone or any device which can be associated to a Wi-Fi access point in order to quickly analyze and save them.
|
||||
This can be used to verify if there is any suspect or malicious communication from a smartphone, by using heuristics or specific Indicators of Compromise (IoCs).</p>
|
||||
|
||||
<p>This little backend allows you to manage the configuration of your TinyCheck instance. You can push some IOCs for detection and whitelist elements which can be seen during legit communications in order to prevent false positives.</p>
|
||||
</div>
|
||||
<div class="backend-footer container grid-lg" id="copyright">
|
||||
<p>Created with <span class="text-error">♥</span> by <a href="https://twitter.com/felixaime" target="_blank">Félix Aimé</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
254
app/backend/src/views/iocs-manage.vue
Normal file
@ -0,0 +1,254 @@
|
||||
<template>
|
||||
<div class="backend-content" id="content">
|
||||
<div class="column col-6 col-xs-12">
|
||||
<h3 class="s-title">Manage IOCs</h3>
|
||||
<ul class="tab tab-block">
|
||||
<li class="tab-item">
|
||||
<a href="#" v-on:click="switch_tab('bulk')" v-bind:class="{ active: tabs.bulk }">Bulk import</a>
|
||||
</li>
|
||||
<li class="tab-item">
|
||||
<a href="#" v-on:click="switch_tab('file')" v-bind:class="{ active: tabs.file }">File import</a>
|
||||
</li>
|
||||
<li class="tab-item">
|
||||
<a href="#" v-on:click="switch_tab('export')" v-bind:class="{ active: tabs.export }">Export IOCs</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div v-if="tabs.export">
|
||||
<iframe :src="export_url" class="frame-export"></iframe>
|
||||
</div>
|
||||
<div v-if="tabs.file">
|
||||
<label class="form-upload empty" for="upload">
|
||||
<input type="file" class="upload-field" id="upload" @change="import_from_file">
|
||||
<p class="empty-title h5">Drop or select a file to import.</p>
|
||||
<p class="empty-subtitle">The file needs to be an export from a TinyCheck instance.</p>
|
||||
</label>
|
||||
</div>
|
||||
<div v-if="tabs.bulk">
|
||||
<div class="columns">
|
||||
<div class="column col-4 col-xs-4">
|
||||
<div class="form-group">
|
||||
<select class="form-select" placeholder="test" v-model="tag">
|
||||
<option value="">IOC(s) Tag</option>
|
||||
<option v-for="t in tags" :value="t.tag" :key="t.tag">
|
||||
{{ t.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column col-4 col-xs-4">
|
||||
<div class="form-group">
|
||||
<select class="form-select width-full" placeholder="test" v-model="type">
|
||||
<option value="">IOC(s) Type</option>
|
||||
<option value="unknown">Multiple (regex parsing)</option>
|
||||
<option v-for="t in types" :value="t.type" :key="t.type">
|
||||
{{ t.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column col-4 col-xs-4">
|
||||
<div class="form-group">
|
||||
<select class="form-select width-full" placeholder="test" v-model="tlp">
|
||||
<option value="">IOC(s) TLP</option>
|
||||
<option value="white">TLP:WHITE</option>
|
||||
<option value="green">TLP:GREEN</option>
|
||||
<option value="amber">TLP:AMBER</option>
|
||||
<option value="red">TLP:RED</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<textarea class="form-input" id="input-example-3" placeholder="Paste your Indicators of Compromise here" rows="15" v-model="iocs"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<button class="btn-primary btn col-12" v-on:click="import_from_bulk()">Import the IOCs</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" v-if="imported.length>0">
|
||||
<div class="toast toast-success">
|
||||
✓ {{imported.length}} IOC<span v-if="errors.length>1">s</span> imported successfully.
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="errors.length>0">
|
||||
<div class="form-group">
|
||||
<div class="toast toast-error">
|
||||
✗ {{errors.length}} IOC<span v-if="errors.length>1">s</span> not imported, see details below.
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Indicator</th>
|
||||
<th>Importation error</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="e in errors" v-bind:key="e.ioc">
|
||||
<td>{{ e.ioc }}</td>
|
||||
<td>{{ e.message }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="type_tag_error==true">
|
||||
<div class="form-group">
|
||||
<div class="toast toast-error">
|
||||
✗ IOC(s) not imported, see details below.
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="empty">
|
||||
<p class="empty-title h5">Please select a tag and a type.</p>
|
||||
<p class="empty-subtitle">If different IOCs types, select "Unknown (regex parsing)".</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
|
||||
export default {
|
||||
name: 'manageiocs',
|
||||
data() {
|
||||
return {
|
||||
type:"",
|
||||
tag:"",
|
||||
tlp:"",
|
||||
iocs:"",
|
||||
types:[],
|
||||
tags:[],
|
||||
errors:[],
|
||||
imported:[],
|
||||
type_tag_error: false,
|
||||
wrong_ioc_file: false,
|
||||
tabs: { "bulk" : true, "file" : false, "export" : false },
|
||||
jwt:"",
|
||||
export_url:"",
|
||||
config: {},
|
||||
watcher: ""
|
||||
}
|
||||
},
|
||||
props: { },
|
||||
methods: {
|
||||
import_from_bulk: function() {
|
||||
this.errors = []
|
||||
this.imported = []
|
||||
if (this.tag != "" && this.type != "" && this.tlp != ""){
|
||||
this.iocs.match(/[^\r\n]+/g).forEach(ioc => {
|
||||
this.import_ioc(this.tag, this.type, this.tlp, ioc);
|
||||
});
|
||||
this.iocs = "";
|
||||
} else {
|
||||
this.type_tag_error = true
|
||||
}
|
||||
},
|
||||
import_ioc: function(tag, type, tlp, ioc) {
|
||||
if (ioc != "" && ioc.slice(0,1) != "#"){
|
||||
if("alert " != ioc.slice(0,6)) {
|
||||
ioc = ioc.trim()
|
||||
ioc = ioc.replace(" ", "")
|
||||
ioc = ioc.replace("[", "")
|
||||
ioc = ioc.replace("]", "")
|
||||
ioc = ioc.replace("\\", "")
|
||||
ioc = ioc.replace("(", "")
|
||||
ioc = ioc.replace(")", "")
|
||||
}
|
||||
axios.get(`/api/ioc/add/${type.trim()}/${tag.trim()}/${tlp.trim()}/${ioc}`, { timeout: 10000, headers: {'X-Token': this.jwt} })
|
||||
.then(response => {
|
||||
if(response.data.status){
|
||||
this.imported.push(response.data);
|
||||
} else if (response.data.message){
|
||||
this.errors.push(response.data);
|
||||
}
|
||||
})
|
||||
.catch(err => (console.log(err)))
|
||||
}
|
||||
},
|
||||
delete_watcher: function(watcher) {
|
||||
var i = this.config.watchers.indexOf(watcher);
|
||||
this.config.watchers.splice(i, 1);
|
||||
},
|
||||
add_watcher: function() {
|
||||
this.config.watchers.push(this.watcher);
|
||||
this.watcher = "";
|
||||
},
|
||||
enrich_selects: function() {
|
||||
axios.get(`/api/ioc/get/tags`, { timeout: 10000, headers: {'X-Token': this.jwt} })
|
||||
.then(response => {
|
||||
if(response.data.tags) this.tags = response.data.tags
|
||||
})
|
||||
.catch(err => (console.log(err)));
|
||||
axios.get(`/api/ioc/get/types`, { timeout: 10000, headers: {'X-Token': this.jwt} })
|
||||
.then(response => {
|
||||
if(response.data.types) this.types = response.data.types
|
||||
})
|
||||
.catch(err => (console.log(err)));
|
||||
},
|
||||
switch_tab: function(tab) {
|
||||
this.errors = []
|
||||
this.imported = []
|
||||
|
||||
Object.keys(this.tabs).forEach(key => {
|
||||
if( key == tab ){
|
||||
this.tabs[key] = true
|
||||
} else {
|
||||
this.tabs[key] = false
|
||||
}
|
||||
});
|
||||
},
|
||||
import_from_file: function(ev) {
|
||||
this.errors = []
|
||||
this.imported = []
|
||||
|
||||
const file = ev.target.files[0];
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = e => this.$emit("load", e.target.result);
|
||||
reader.onload = () => {
|
||||
try {
|
||||
JSON.parse(reader.result).iocs.forEach(ioc => {
|
||||
this.import_ioc(ioc["tag"], ioc["type"], ioc["tlp"], ioc["value"])
|
||||
})
|
||||
} catch (error) {
|
||||
this.wrong_ioc_file = true
|
||||
}
|
||||
|
||||
}
|
||||
reader.readAsText(file);
|
||||
},
|
||||
async get_jwt(){
|
||||
await axios.get(`/api/get-token`, { timeout: 10000 })
|
||||
.then(response => {
|
||||
if(response.data.token){
|
||||
this.jwt = response.data.token
|
||||
}
|
||||
})
|
||||
.catch(err => (console.log(err)))
|
||||
},
|
||||
load_config: function() {
|
||||
axios.get(`/api/config/list`, {
|
||||
timeout: 10000,
|
||||
headers: { 'X-Token': this.jwt }
|
||||
}).then(response => {
|
||||
if (response.data) {
|
||||
this.config = response.data
|
||||
}
|
||||
})
|
||||
.catch(err => (console.log(err)))
|
||||
}
|
||||
},
|
||||
created: function() {
|
||||
this.get_jwt().then(() => {
|
||||
this.enrich_selects();
|
||||
this.load_config();
|
||||
this.export_url = `/api/ioc/export?token=${this.jwt}`
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
114
app/backend/src/views/iocs-search.vue
Normal file
@ -0,0 +1,114 @@
|
||||
<template>
|
||||
<div class="backend-content" id="content">
|
||||
<div class="column col-8 col-xs-12">
|
||||
<h3 class="s-title">Search IOCs</h3>
|
||||
<div class="form-group">
|
||||
<textarea class="form-input" id="input-example-3" placeholder="Paste your IOCs here" rows="3" v-model="iocs"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<button class="btn btn-primary col-12" v-on:click="search_iocs()">Search</button>
|
||||
</div>
|
||||
<div class="form-group" v-if="results.length>0 ">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Indicator</th>
|
||||
<th>Type</th>
|
||||
<th>Tag</th>
|
||||
<th>TLP</th>
|
||||
<th> </th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="r in results" :key="r.tlp">
|
||||
<td>{{ r.value }}</td>
|
||||
<td class="capi">{{ r.type }}</td>
|
||||
<td class="upper">{{ r.tag }}</td>
|
||||
<td><label :class="['tlp-' + r.tlp]">{{ r.tlp }}</label></td>
|
||||
<td><button class="btn btn-sm" v-on:click="remove(r)">Delete</button></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div v-else-if="first_search==false">
|
||||
<div class="empty">
|
||||
<p class="empty-title h5">IOC<span v-if="this.iocs.match(/[^\r\n]+/g).length>1">s</span> not found.</p>
|
||||
<p class="empty-subtitle">Try wildcard search to expend your search.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
|
||||
export default {
|
||||
name: 'iocs-search',
|
||||
data() {
|
||||
return {
|
||||
results: [],
|
||||
first_search: true,
|
||||
jwt:""
|
||||
}
|
||||
},
|
||||
props: { },
|
||||
methods: {
|
||||
search_iocs: function() {
|
||||
this.results = []
|
||||
this.first_search = false
|
||||
this.iocs.match(/[^\r\n]+/g).forEach(ioc => {
|
||||
ioc = ioc.trim()
|
||||
if("alert " != ioc.slice(0,6)) {
|
||||
ioc = ioc.replace(" ", "")
|
||||
ioc = ioc.replace("[", "")
|
||||
ioc = ioc.replace("]", "")
|
||||
ioc = ioc.replace("\\", "")
|
||||
ioc = ioc.replace("(", "")
|
||||
ioc = ioc.replace(")", "")
|
||||
}
|
||||
axios.get(`/api/ioc/search/${ioc}`, { timeout: 10000, headers: {'X-Token': this.jwt} })
|
||||
.then(response => {
|
||||
if(response.data.results.length>0){
|
||||
this.results = [].concat(this.results, response.data.results);
|
||||
}
|
||||
})
|
||||
.catch(err => (console.log(err)))
|
||||
});
|
||||
return true;
|
||||
},
|
||||
remove: function(elem){
|
||||
axios.get(`/api/ioc/delete/${elem.id}`, { timeout: 10000, headers: {'X-Token': this.jwt} })
|
||||
.then(response => {
|
||||
if(response.data.status){
|
||||
this.results = this.results.filter(function(el) { return el != elem; });
|
||||
}
|
||||
})
|
||||
.catch(err => (console.log(err)))
|
||||
},
|
||||
load_config: function() {
|
||||
axios.get(`/api/config/list`, {
|
||||
timeout: 10000,
|
||||
headers: { 'X-Token': this.jwt }
|
||||
}).then(response => {
|
||||
if (response.data) {
|
||||
this.config = response.data
|
||||
}
|
||||
})
|
||||
.catch(err => (console.log(err)))
|
||||
},
|
||||
async get_jwt(){
|
||||
await axios.get(`/api/get-token`, { timeout: 10000 })
|
||||
.then(response => {
|
||||
if(response.data.token){
|
||||
this.jwt = response.data.token
|
||||
}
|
||||
})
|
||||
.catch(err => (console.log(err)))
|
||||
}
|
||||
},
|
||||
created: function() {
|
||||
this.get_jwt()
|
||||
}
|
||||
}
|
||||
</script>
|
116
app/backend/src/views/network-manage.vue
Normal file
@ -0,0 +1,116 @@
|
||||
<template>
|
||||
<div class="backend-content" id="content">
|
||||
<div class="column col-6 col-xs-12">
|
||||
<h3 class="s-title">Network configuration</h3>
|
||||
<h5 class="s-subtitle">Interfaces configuration</h5>
|
||||
<img src="@/assets/network.png" id="network-thumbnail" />
|
||||
<div class="container interfaces-container">
|
||||
<div class="columns">
|
||||
<div class="column col-6">
|
||||
<span class="interface-label">First interface</span>
|
||||
<div class="form-group">
|
||||
<div class="btn-group btn-group-block">
|
||||
<button class="btn btn-sm btn-iface" @click="change_interface('in', iface)" :class="iface == config.network.in ? 'active' : ''" v-for="iface in config.interfaces" :key="iface">{{ iface.toUpperCase() }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column col-6">
|
||||
<span class="interface-label">Second interface</span>
|
||||
<div class="form-group">
|
||||
<div class="btn-group btn-group-block">
|
||||
<button class="btn btn-sm btn-iface" @click="change_interface('out', iface)" :class="iface == config.network.out ? 'active' : ''" v-for="iface in config.interfaces" :key="iface">{{ iface.toUpperCase() }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h5 class="s-subtitle">Edit SSIDs names</h5>
|
||||
<div class="form-group">
|
||||
<table class="table table-striped table-hover">
|
||||
<tbody>
|
||||
<tr v-for="ssid in config.network.ssids" :key="ssid">
|
||||
<td>{{ ssid }}</td>
|
||||
<td><button class="btn btn-sm" v-on:click="delete_ssid(ssid)">Delete</button></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><input class="form-input" v-model="ssid" type="text" placeholder="SSID name"></td>
|
||||
<td><button class="btn btn-sm" @click="add_ssid()">Add</button></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
|
||||
export default {
|
||||
name: 'manageinterface',
|
||||
data() {
|
||||
return {
|
||||
config: {},
|
||||
ssid: ""
|
||||
}
|
||||
},
|
||||
props: {},
|
||||
methods: {
|
||||
async get_jwt() {
|
||||
await axios.get(`/api/get-token`, { timeout: 10000 })
|
||||
.then(response => {
|
||||
if (response.data.token) {
|
||||
this.jwt = response.data.token
|
||||
}
|
||||
})
|
||||
.catch(err => (console.log(err)))
|
||||
},
|
||||
load_config: function() {
|
||||
axios.get(`/api/config/list`, {
|
||||
timeout: 10000,
|
||||
headers: { 'X-Token': this.jwt }
|
||||
}).then(response => {
|
||||
if (response.data) {
|
||||
this.config = response.data
|
||||
}
|
||||
})
|
||||
.catch(err => (console.log(err)))
|
||||
},
|
||||
delete_ssid: function(ssid) {
|
||||
var i = this.config.network.ssids.indexOf(ssid);
|
||||
this.config.network.ssids.splice(i, 1);
|
||||
this.update_ssids();
|
||||
},
|
||||
add_ssid: function() {
|
||||
this.config.network.ssids.push(this.ssid);
|
||||
this.ssid = "";
|
||||
this.update_ssids();
|
||||
},
|
||||
update_ssids: function() {
|
||||
axios.get(`/api/config/edit/network/ssids/${this.config.network.ssids.join("|")}`, {
|
||||
timeout: 10000,
|
||||
headers: { 'X-Token': this.jwt }
|
||||
}).then(response => {
|
||||
if (response.data.status) {
|
||||
console.log(response.data)
|
||||
}
|
||||
})
|
||||
.catch(err => (console.log(err)))
|
||||
},
|
||||
change_interface: function(type, iface) {
|
||||
axios.get(`/api/config/edit/network/${type}/${iface}`, {
|
||||
timeout: 10000,
|
||||
headers: { 'X-Token': this.jwt }
|
||||
}).then(response => {
|
||||
if (response.data.status) this.config.network[type] = iface
|
||||
})
|
||||
.catch(err => (console.log(err)))
|
||||
},
|
||||
},
|
||||
created: function() {
|
||||
this.get_jwt().then(() => {
|
||||
this.load_config();
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
191
app/backend/src/views/whitelist-manage.vue
Normal file
@ -0,0 +1,191 @@
|
||||
<template>
|
||||
<div class="backend-content" id="content">
|
||||
<div class="column col-6 col-xs-12">
|
||||
<h3 class="s-title">Manage whitelisted elements</h3>
|
||||
<ul class="tab tab-block">
|
||||
<li class="tab-item">
|
||||
<a href="#" v-on:click="switch_tab('bulk')" v-bind:class="{ active: tabs.bulk }">Bulk elements import</a>
|
||||
</li>
|
||||
<li class="tab-item">
|
||||
<a href="#" v-on:click="switch_tab('file')" v-bind:class="{ active: tabs.file }">Import from file</a>
|
||||
</li>
|
||||
<li class="tab-item">
|
||||
<a href="#" v-on:click="switch_tab('export')" v-bind:class="{ active: tabs.export }">Export elements</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div v-if="tabs.export">
|
||||
<iframe :src="export_url" class="frame-export"></iframe>
|
||||
</div>
|
||||
<div v-if="tabs.file">
|
||||
<label class="form-upload empty" for="upload">
|
||||
<input type="file" class="upload-field" id="upload" @change="import_from_file">
|
||||
<p class="empty-title h5">Drop or select a file to import.</p>
|
||||
<p class="empty-subtitle">The file needs to be an whitelist file export from a TinyCheck instance.</p>
|
||||
</label>
|
||||
</div>
|
||||
<div v-if="tabs.bulk">
|
||||
<div class="form-group">
|
||||
<select class="form-select width-full" placeholder="test" v-model="type">
|
||||
<option value="">Elements Type</option>
|
||||
<option value="unknown">Multiple (regex parsing)</option>
|
||||
<option v-for="t in types" :value="t.type" :key="t.type">
|
||||
{{ t.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<textarea class="form-input" id="input-example-3" placeholder="Paste the elements to be whitelisted here" rows="15" v-model="elements"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<button class="btn-primary btn col-12" v-on:click="import_from_bulk()">Whitelist elements</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" v-if="imported.length>0">
|
||||
<div class="toast toast-success">
|
||||
✓ {{imported.length}} IOC<span v-if="errors.length>1">s</span> imported successfully.
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="errors.length>0">
|
||||
<div class="form-group">
|
||||
<div class="toast toast-error">
|
||||
✗ {{errors.length}} IOC<span v-if="errors.length>1">s</span> not imported, see details below.
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Element</th>
|
||||
<th>Importation error</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="e in errors" :key="e.element">
|
||||
<td>{{ e.element }}</td>
|
||||
<td>{{ e.message }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="type_tag_error==true">
|
||||
<div class="form-group">
|
||||
<div class="toast toast-error">
|
||||
✗ IOC(s) not imported, see details below.
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="empty">
|
||||
<p class="empty-title h5">Please select a tag and a type.</p>
|
||||
<p class="empty-subtitle">If different IOCs types, select "Unknown (regex parsing)".</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
|
||||
export default {
|
||||
name: 'manageiocs',
|
||||
data() {
|
||||
return {
|
||||
type:"",
|
||||
elements:"",
|
||||
types:[],
|
||||
errors:[],
|
||||
imported:[],
|
||||
wrong_wh_file: false,
|
||||
tabs: { "bulk" : true, "file" : false, "export" : false },
|
||||
jwt:"",
|
||||
export_url:""
|
||||
}
|
||||
},
|
||||
props: { },
|
||||
methods: {
|
||||
import_from_bulk: function() {
|
||||
this.errors = []
|
||||
this.imported = []
|
||||
if (this.type != ""){
|
||||
this.elements.match(/[^\r\n]+/g).forEach(elem => {
|
||||
this.import_element(this.type, elem);
|
||||
});
|
||||
this.elements = "";
|
||||
} else {
|
||||
this.type_tag_error = true
|
||||
}
|
||||
},
|
||||
import_element: function(type, elem) {
|
||||
if (elem != "" && elem.slice(0,1) != "#"){
|
||||
axios.get(`/api/whitelist/add/${type.trim()}/${elem.trim()}`, {
|
||||
timeout: 10000,
|
||||
headers: { "X-Token" : this.jwt }
|
||||
}).then(response => {
|
||||
if(response.data.status){
|
||||
this.imported.push(response.data);
|
||||
} else if (response.data.message){
|
||||
this.errors.push(response.data);
|
||||
}
|
||||
})
|
||||
.catch(err => (console.log(err)))
|
||||
}
|
||||
},
|
||||
enrich_types: function() {
|
||||
axios.get(`/api/whitelist/get/types`, { timeout: 10000, headers: {'X-Token': this.jwt} })
|
||||
.then(response => {
|
||||
if(response.data.types) this.types = response.data.types
|
||||
})
|
||||
.catch(err => (console.log(err)));
|
||||
},
|
||||
switch_tab: function(tab) {
|
||||
this.errors = []
|
||||
this.imported = []
|
||||
|
||||
Object.keys(this.tabs).forEach(key => {
|
||||
if( key == tab ){
|
||||
this.tabs[key] = true
|
||||
} else {
|
||||
this.tabs[key] = false
|
||||
}
|
||||
});
|
||||
},
|
||||
import_from_file: function(ev) {
|
||||
this.errors = []
|
||||
this.imported = []
|
||||
|
||||
const file = ev.target.files[0];
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = e => this.$emit("load", e.target.result);
|
||||
reader.onload = () => {
|
||||
try {
|
||||
JSON.parse(reader.result).elements.forEach(elem => {
|
||||
this.import_element(elem["type"], elem["element"])
|
||||
})
|
||||
} catch (error) {
|
||||
this.wrong_wh_file = true
|
||||
}
|
||||
|
||||
}
|
||||
reader.readAsText(file);
|
||||
},
|
||||
async get_jwt(){
|
||||
await axios.get(`/api/get-token`, { timeout: 10000 })
|
||||
.then(response => {
|
||||
if(response.data.token){
|
||||
this.jwt = response.data.token
|
||||
}
|
||||
})
|
||||
.catch(err => (console.log(err)))
|
||||
}
|
||||
},
|
||||
created: function() {
|
||||
this.get_jwt().then(() => {
|
||||
this.enrich_types();
|
||||
this.export_url = `/api/whitelist/export?token=${this.jwt}`
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
94
app/backend/src/views/whitelist-search.vue
Normal file
@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<div class="backend-content" id="content">
|
||||
<div class="column col-6 col-xs-12">
|
||||
<h3 class="s-title">Search whitelisted elements</h3>
|
||||
<div class="form-group">
|
||||
<textarea class="form-input" id="input-example-3" placeholder="Paste the elements here" rows="3" v-model="elements"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<button class="btn btn-primary col-12" v-on:click="search_elements()">Search</button>
|
||||
</div>
|
||||
<div class="form-group" v-if="results.length>0 ">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Element</th>
|
||||
<th>Element type</th>
|
||||
<th> </th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="r in results" :key="r.element">
|
||||
<td>{{ r.element }}</td>
|
||||
<td>{{ r.type }}</td>
|
||||
<td><button class="btn btn-sm" v-on:click="remove(r)">Delete</button></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div v-else-if="first_search==false">
|
||||
<div class="empty">
|
||||
<p class="empty-title h5">Element<span v-if="this.elements.match(/[^\r\n]+/g).length>1">s</span> not found.</p>
|
||||
<p class="empty-subtitle">Try wildcard search to expend your search.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
|
||||
export default {
|
||||
name: 'elements-search',
|
||||
data() {
|
||||
return {
|
||||
results: [],
|
||||
first_search: true,
|
||||
jwt:""
|
||||
}
|
||||
},
|
||||
props: { },
|
||||
methods: {
|
||||
search_elements: function() {
|
||||
this.results = []
|
||||
this.first_search = false
|
||||
this.elements.match(/[^\r\n]+/g).forEach(elem => {
|
||||
axios.get(`/api/whitelist/search/${elem.trim()}`, {
|
||||
timeout: 10000,
|
||||
headers: {'X-Token': this.jwt}
|
||||
}).then(response => {
|
||||
if(response.data.results.length>0){
|
||||
this.results = [].concat(this.results, response.data.results);
|
||||
}
|
||||
})
|
||||
.catch(err => (console.log(err)))
|
||||
});
|
||||
return true;
|
||||
},
|
||||
remove: function(elem){
|
||||
axios.get(`/api/whitelist/delete/${elem.id}`, {
|
||||
timeout: 10000,
|
||||
headers: {'X-Token': this.jwt}
|
||||
}).then(response => {
|
||||
if(response.data.status){
|
||||
this.results = this.results.filter(function(el) { return el != elem; });
|
||||
}
|
||||
})
|
||||
.catch(err => (console.log(err)))
|
||||
},
|
||||
async get_jwt(){
|
||||
await axios.get(`/api/get-token`, { timeout: 10000 })
|
||||
.then(response => {
|
||||
if(response.data.token){
|
||||
this.jwt = response.data.token
|
||||
}
|
||||
})
|
||||
.catch(err => (console.log(err)))
|
||||
}
|
||||
},
|
||||
created: function() {
|
||||
this.get_jwt()
|
||||
}
|
||||
}
|
||||
</script>
|
11
app/backend/vue.config.js
Normal file
@ -0,0 +1,11 @@
|
||||
module.exports = {
|
||||
devServer: {
|
||||
proxy: {
|
||||
'^/api': {
|
||||
target: 'https://localhost:5000',
|
||||
ws: true,
|
||||
changeOrigin: true
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
24
app/frontend/README.md
Normal file
@ -0,0 +1,24 @@
|
||||
# tinycheck-new
|
||||
|
||||
## Project setup
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
### Compiles and hot-reloads for development
|
||||
```
|
||||
npm run serve
|
||||
```
|
||||
|
||||
### Compiles and minifies for production
|
||||
```
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Lints and fixes files
|
||||
```
|
||||
npm run lint
|
||||
```
|
||||
|
||||
### Customize configuration
|
||||
See [Configuration Reference](https://cli.vuejs.org/config/).
|
5
app/frontend/babel.config.js
Normal file
@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
'@vue/cli-plugin-babel/preset'
|
||||
]
|
||||
}
|
12248
app/frontend/package-lock.json
generated
Normal file
48
app/frontend/package.json
Normal file
@ -0,0 +1,48 @@
|
||||
{
|
||||
"name": "tinycheck-new",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve --copy --port=4202",
|
||||
"build": "vue-cli-service build",
|
||||
"lint": "vue-cli-service lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fnando/sparkline": "^0.3.10",
|
||||
"axios": "^0.20.0",
|
||||
"core-js": "^3.6.5",
|
||||
"sass": "^1.27.0",
|
||||
"sass-loader": "^10.0.4",
|
||||
"simple-keyboard": "^2.30.25",
|
||||
"vue": "^2.6.12",
|
||||
"vue-router": "^3.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-plugin-babel": "~4.5.6",
|
||||
"@vue/cli-plugin-eslint": "~4.5.6",
|
||||
"@vue/cli-service": "~4.5.6",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"eslint": "^7.9.0",
|
||||
"eslint-plugin-vue": "^6.2.2",
|
||||
"vue-template-compiler": "^2.6.12"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
"env": {
|
||||
"node": true
|
||||
},
|
||||
"extends": [
|
||||
"plugin:vue/essential",
|
||||
"eslint:recommended"
|
||||
],
|
||||
"parserOptions": {
|
||||
"parser": "babel-eslint"
|
||||
},
|
||||
"rules": {}
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"last 2 versions",
|
||||
"not dead"
|
||||
]
|
||||
}
|
3
app/frontend/public/.vscode/settings.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"python.pythonPath": "/usr/local/opt/python@3.8/bin/python3.8"
|
||||
}
|
30
app/frontend/src/App.vue
Normal file
@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<transition name="fade" mode="out-in">
|
||||
<router-view />
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
@import './assets/spectre.min.css';
|
||||
@import './assets/custom.css';
|
||||
|
||||
/* Face style for router stuff. */
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition-duration: 0.3s;
|
||||
transition-property: opacity;
|
||||
transition-timing-function: ease;
|
||||
}
|
||||
|
||||
.fade-enter,
|
||||
.fade-leave-active {
|
||||
opacity: 0
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
document.title = 'TinyCheck Frontend'
|
||||
</script>
|
||||
|
636
app/frontend/src/assets/custom.css
Normal file
@ -0,0 +1,636 @@
|
||||
|
||||
@font-face {
|
||||
font-family: 'Lobster';
|
||||
font-weight: normal;
|
||||
src: url('fonts/lobster.ttf') format('truetype');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Roboto-Bold";
|
||||
src: url("fonts/Roboto-Bold.eot"); /* IE9 Compat Modes */
|
||||
src: url("fonts/Roboto-Bold.eot?#iefix") format("embedded-opentype"), /* IE6-IE8 */
|
||||
url("fonts/Roboto-Bold.otf") format("opentype"), /* Open Type Font */
|
||||
url("fonts/Roboto-Bold.svg") format("svg"), /* Legacy iOS */
|
||||
url("fonts/Roboto-Bold.ttf") format("truetype"), /* Safari, Android, iOS */
|
||||
url("fonts/Roboto-Bold.woff") format("woff"), /* Modern Browsers */
|
||||
url("fonts/Roboto-Bold.woff2") format("woff2"); /* Modern Browsers */
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Roboto-Regular";
|
||||
src: url("fonts/Roboto-Regular.eot"); /* IE9 Compat Modes */
|
||||
src: url("fonts/Roboto-Regular.eot?#iefix") format("embedded-opentype"), /* IE6-IE8 */
|
||||
url("fonts/Roboto-Regular.otf") format("opentype"), /* Open Type Font */
|
||||
url("fonts/Roboto-Regular.svg") format("svg"), /* Legacy iOS */
|
||||
url("fonts/Roboto-Regular.ttf") format("truetype"), /* Safari, Android, iOS */
|
||||
url("fonts/Roboto-Regular.woff") format("woff"), /* Modern Browsers */
|
||||
url("fonts/Roboto-Regular.woff2") format("woff2"); /* Modern Browsers */
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
* {
|
||||
font-family: "Roboto-Regular";
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
#qrcode {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
margin-top:10px;
|
||||
margin-left:5px;
|
||||
border:1px #000;
|
||||
border-radius:5px;
|
||||
}
|
||||
|
||||
.card {
|
||||
border: 0;
|
||||
box-shadow: 0 0.25rem 1rem rgba(48,55,66,.15);
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.empty-subtitle {
|
||||
font-family: "Roboto";
|
||||
}
|
||||
|
||||
.btn {
|
||||
font-family: 'Lobster';
|
||||
font-size:30px;
|
||||
}
|
||||
|
||||
/*
|
||||
The min Chrome with is not 480 but 500 so we have to define our app div
|
||||
with 480 width on a 480 screen.
|
||||
*/
|
||||
@media only screen and (max-width: 500px) {
|
||||
|
||||
#app {
|
||||
height: 320px;
|
||||
display:block;
|
||||
width: 480px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
#tinycheck-logo {
|
||||
width:370px;
|
||||
}
|
||||
|
||||
.apcard {
|
||||
width: 400px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
height: 170px;
|
||||
display: block;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.sparklines-container {
|
||||
display:flex;
|
||||
flex-direction:row;
|
||||
flex-wrap: wrap;
|
||||
justify-content:center;
|
||||
align-items:center;
|
||||
align-content:center;
|
||||
width: 480px;
|
||||
}
|
||||
|
||||
.keyboardinput {
|
||||
padding:15px;
|
||||
}
|
||||
|
||||
* {
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.btn {
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.warning-title {
|
||||
font-family: "lobster";
|
||||
font-weight: lighter;
|
||||
font-size: 28px;
|
||||
margin-bottom: .5em;
|
||||
margin-top: .5em;
|
||||
color:#FFF;
|
||||
text-shadow: 1px 2px 3px #0000005c;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.report-wrapper {
|
||||
width:90%;
|
||||
margin:auto;
|
||||
}
|
||||
|
||||
.device-ctx {
|
||||
width:90%;
|
||||
margin:20px 0px 20px 10px;
|
||||
}
|
||||
|
||||
.btn-save {
|
||||
width:90%;
|
||||
margin:auto;
|
||||
}
|
||||
|
||||
.high-wrapper {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #e53935;
|
||||
}
|
||||
|
||||
.med-wrapper {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #f1602b;
|
||||
}
|
||||
|
||||
.low-wrapper, .none-wrapper {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #56ab2f;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 501px) {
|
||||
|
||||
#app {
|
||||
height: 100%;
|
||||
position:absolute;
|
||||
width: 100%;
|
||||
overflow-x: hidden;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.apcard {
|
||||
width: 500px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
height: fit-content;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
#tinycheck-logo {
|
||||
width:500px;
|
||||
}
|
||||
|
||||
.warning-title {
|
||||
font-family: "lobster";
|
||||
font-weight: lighter;
|
||||
font-weight: bold;
|
||||
margin-bottom: .5em;
|
||||
margin-top: .5em;
|
||||
color:#FFF;
|
||||
text-shadow: 1px 2px 3px #0000005c;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.report-wrapper {
|
||||
width:60%;
|
||||
margin:auto;
|
||||
}
|
||||
|
||||
.device-ctx {
|
||||
padding:15px;
|
||||
margin:auto;
|
||||
}
|
||||
|
||||
.btn-save {
|
||||
margin:auto;
|
||||
}
|
||||
|
||||
.high-wrapper {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(to right, #e53935, #e35d5b);
|
||||
}
|
||||
|
||||
.med-wrapper {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(to right, #ff4b1f, #ff9068);
|
||||
}
|
||||
|
||||
.low-wrapper, .none-wrapper {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(to right, #56ab2f, #a8e063);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.width-100 {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.center {
|
||||
width: fit-content;
|
||||
position: relative;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
height: fit-content;
|
||||
text-align:center;
|
||||
}
|
||||
|
||||
.light-grey {
|
||||
color:#999;
|
||||
}
|
||||
|
||||
.timer {
|
||||
font-size:40px;
|
||||
font-weight: 100;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #f7f8f9;
|
||||
}
|
||||
|
||||
footer {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding-left:40px;
|
||||
padding-right:40px;
|
||||
padding-bottom:40px;
|
||||
}
|
||||
|
||||
.container-padding-right {
|
||||
padding-left:40px;
|
||||
}
|
||||
|
||||
.group-title {
|
||||
text-transform: uppercase;
|
||||
color : #999;
|
||||
font-size:12px;
|
||||
display: block;
|
||||
padding-bottom:10px;
|
||||
padding-top:30px;
|
||||
}
|
||||
|
||||
.keyboardinput {
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
padding: 20px;
|
||||
font-size: 20px;
|
||||
border: none;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.legend {
|
||||
color:#9a9a9a;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
/* Scrollbar */
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 15px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #CCC;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
/* Spectre CSS tweaks */
|
||||
|
||||
.btn {
|
||||
height: fit-content;
|
||||
border-radius: 5px;
|
||||
padding: .4rem .7rem;
|
||||
font-size: 1rem;
|
||||
color: #4c4c4c;
|
||||
background:#f7f8f9;
|
||||
border: 1px solid #4c4c4c;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
height: fit-content;
|
||||
border-radius: 5px;
|
||||
padding: .4rem .7rem;
|
||||
font-size: 1rem;
|
||||
color: #4c4c4c;
|
||||
background:#FFF;
|
||||
border: 1px solid #4c4c4c;
|
||||
}
|
||||
|
||||
.btn.active,.btn:active {
|
||||
height: fit-content;
|
||||
border-radius: 5px;
|
||||
padding: .4rem .7rem;
|
||||
font-size: 1rem;
|
||||
color: #4c4c4c;
|
||||
background:#FFF;
|
||||
border: 1px solid #4c4c4c;
|
||||
}
|
||||
|
||||
.btn.btn-light {
|
||||
background: #f9f9f9;
|
||||
border-color: #c1c1c1;
|
||||
color: #c1c1c1;
|
||||
}
|
||||
|
||||
.loadingsplash {
|
||||
margin-top:20px;
|
||||
}
|
||||
|
||||
.loading::after {
|
||||
animation: loading .5s infinite linear;
|
||||
background: 0 0;
|
||||
border: .1rem solid #000;
|
||||
border-radius: 50%;
|
||||
border-right-color: transparent;
|
||||
border-top-color: transparent;
|
||||
content: "";
|
||||
display: block;
|
||||
height: .8rem;
|
||||
left: 50%;
|
||||
margin-left: -.4rem;
|
||||
margin-top: -.4rem;
|
||||
opacity: 1;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
width: .8rem;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.loading.loading-lg::after {
|
||||
height: 1.6rem;
|
||||
margin-left: -.8rem;
|
||||
margin-top: -.8rem;
|
||||
width: 1.6rem;
|
||||
}
|
||||
|
||||
.divider-vert[data-content]::after, .divider[data-content]::after {
|
||||
background: #f7f8f9;
|
||||
}
|
||||
|
||||
.white-bg[data-content]::after, .white-bg[data-content]::after {
|
||||
background: #FFFFFF;
|
||||
}
|
||||
|
||||
#sparkline {
|
||||
stroke: #e8e8e8;
|
||||
fill: #f1f1f1;
|
||||
bottom: 0;
|
||||
position: fixed;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.capture-wrapper {
|
||||
display: block;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn.btn-primary {
|
||||
border-radius: 5px;
|
||||
border: 1px solid #333;
|
||||
color: #FAFAFA;
|
||||
background-color: #333;
|
||||
}
|
||||
|
||||
.btn.btn-primary:hover {
|
||||
border-radius: 5px;
|
||||
border: 1px solid rgb(87, 87, 87);
|
||||
color: #FAFAFA;
|
||||
background-color: rgb(87, 87, 87);
|
||||
}
|
||||
|
||||
.btn.btn-primary:active {
|
||||
border-radius: 5px;
|
||||
border: 1px solid rgb(87, 87, 87);
|
||||
color: #FAFAFA;
|
||||
background-color: rgb(87, 87, 87);
|
||||
}
|
||||
|
||||
.btn-report-high {
|
||||
border-radius: 5px;
|
||||
border: 2px solid #ffffff;
|
||||
color: #e34b49;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.btn-report-high:hover {
|
||||
border-radius: 5px;
|
||||
border: 2px solid #ffffff;
|
||||
color: #e34b49;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.btn-report-moderate {
|
||||
border-radius: 5px;
|
||||
border: 2px solid #ffffff;
|
||||
color: #ff4b1f;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.btn-report-moderate:hover {
|
||||
border-radius: 5px;
|
||||
border: 2px solid #ffffff;
|
||||
color: #ff4b1f;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.btn-report-low {
|
||||
border-radius: 5px;
|
||||
border: 2px solid #ffffff;
|
||||
color: #56ab2f;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.btn-report-low:hover {
|
||||
border-radius: 5px;
|
||||
border: 2px solid #ffffff;
|
||||
color: #56ab2f;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.btn-report-low-light {
|
||||
border-radius: 5px;
|
||||
border: 2px solid #ffffff;
|
||||
color: #ffffff;
|
||||
background-color:transparent;
|
||||
margin-right:10px;
|
||||
}
|
||||
|
||||
.btn-report-low-light:hover {
|
||||
border-radius: 5px;
|
||||
border: 2px solid #ffffff;
|
||||
color: #ffffff;
|
||||
background-color:transparent;
|
||||
margin-right:10px;
|
||||
}
|
||||
|
||||
.alert-body {
|
||||
background-color: #FFF;
|
||||
list-style: none;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid #EEE;
|
||||
}
|
||||
|
||||
.alert-body>.title {
|
||||
display: block;
|
||||
padding: 5px 5px 5px 10px;
|
||||
}
|
||||
|
||||
.high-label {
|
||||
background-color: #e53d38;
|
||||
padding: 5px;
|
||||
text-transform: uppercase;
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
border-radius: 3px 0px 0px 0px;
|
||||
margin: 0px;
|
||||
color: #FFF;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.moderate-label {
|
||||
background-color: #ff7e33eb;
|
||||
padding: 5px;
|
||||
text-transform: uppercase;
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
border-radius: 3px 0px 0px 0px;
|
||||
margin: 0px;
|
||||
color: #FFF;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.low-label {
|
||||
background-color: #4fce0eb8;
|
||||
padding: 5px;
|
||||
text-transform: uppercase;
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
border-radius: 3px 0px 0px 0px;
|
||||
margin: 0px;
|
||||
color: #FFF;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.description {
|
||||
margin: 0;
|
||||
padding: 10px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.description>i {
|
||||
font-style: inherit;
|
||||
color: #353535;
|
||||
padding: 2px 5px 2px 5px;
|
||||
border-radius: 5px;
|
||||
background-color: #F2F2F2;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
margin:0;
|
||||
padding:0;
|
||||
}
|
||||
|
||||
.alert-id {
|
||||
background-color: #636363;
|
||||
padding: 5px;
|
||||
text-transform: uppercase;
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
border-radius: 0px 3px 0px 0px;
|
||||
margin: 0px;
|
||||
color: #FFF;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight:500;
|
||||
}
|
||||
|
||||
.btn-save {
|
||||
margin-top:20px;
|
||||
margin-bottom:20px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#controls-analysis {
|
||||
margin-top: 25px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.lobster {
|
||||
font-family: 'lobster';
|
||||
font-size: 40px;
|
||||
}
|
||||
|
||||
.wifi-login {
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.frame-download {
|
||||
width:1px;
|
||||
height:1px;
|
||||
border:0;
|
||||
}
|
||||
|
||||
.btn:focus {
|
||||
border-color: inherit;
|
||||
-webkit-box-shadow: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.form-select:focus {
|
||||
border-color: inherit;
|
||||
-webkit-box-shadow: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
border-color: inherit;
|
||||
-webkit-box-shadow: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* Used by the legend on analysis */
|
||||
|
||||
.fade-in {
|
||||
animation: fadeIn ease 1s;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
0% {
|
||||
opacity:0;
|
||||
}
|
||||
100% {
|
||||
opacity:1;
|
||||
}
|
||||
}
|
BIN
app/frontend/src/assets/fonts/Roboto-Bold.eot
Normal file
BIN
app/frontend/src/assets/fonts/Roboto-Bold.otf
Normal file
11535
app/frontend/src/assets/fonts/Roboto-Bold.svg
Normal file
After Width: | Height: | Size: 805 KiB |
BIN
app/frontend/src/assets/fonts/Roboto-Bold.ttf
Normal file
BIN
app/frontend/src/assets/fonts/Roboto-Bold.woff
Normal file
BIN
app/frontend/src/assets/fonts/Roboto-Bold.woff2
Normal file
BIN
app/frontend/src/assets/fonts/Roboto-Regular.eot
Normal file
BIN
app/frontend/src/assets/fonts/Roboto-Regular.otf
Normal file
11080
app/frontend/src/assets/fonts/Roboto-Regular.svg
Normal file
After Width: | Height: | Size: 784 KiB |
BIN
app/frontend/src/assets/fonts/Roboto-Regular.ttf
Normal file
BIN
app/frontend/src/assets/fonts/Roboto-Regular.woff
Normal file
BIN
app/frontend/src/assets/fonts/Roboto-Regular.woff2
Normal file
BIN
app/frontend/src/assets/fonts/lobster.ttf
Normal file
BIN
app/frontend/src/assets/icon.png
Normal file
After Width: | Height: | Size: 28 KiB |
4
app/frontend/src/assets/icon_plug_usb.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg width="112" height="195" viewBox="0 0 112 195" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<line x1="3.5" y1="3.5" x2="3.50001" y2="191.5" stroke="black" stroke-width="7" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<rect x="7" width="105" height="195" fill="#F7F8F9"/>
|
||||
</svg>
|
After Width: | Height: | Size: 294 B |
16
app/frontend/src/assets/icon_spinner.svg
Normal file
@ -0,0 +1,16 @@
|
||||
<svg version="1.1" id="loader-1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="40px" height="40px" viewBox="0 0 40 40" enable-background="new 0 0 40 40" xml:space="preserve">
|
||||
<path opacity="0.2" fill="#000" d="M20.201,5.169c-8.254,0-14.946,6.692-14.946,14.946c0,8.255,6.692,14.946,14.946,14.946
|
||||
s14.946-6.691,14.946-14.946C35.146,11.861,28.455,5.169,20.201,5.169z M20.201,31.749c-6.425,0-11.634-5.208-11.634-11.634
|
||||
c0-6.425,5.209-11.634,11.634-11.634c6.425,0,11.633,5.209,11.633,11.634C31.834,26.541,26.626,31.749,20.201,31.749z"/>
|
||||
<path fill="#f7f8f9" d="M26.013,10.047l1.654-2.866c-2.198-1.272-4.743-2.012-7.466-2.012h0v3.312h0
|
||||
C22.32,8.481,24.301,9.057,26.013,10.047z">
|
||||
<animateTransform attributeType="xml"
|
||||
attributeName="transform"
|
||||
type="rotate"
|
||||
from="0 20 20"
|
||||
to="360 20 20"
|
||||
dur="0.5s"
|
||||
repeatCount="indefinite"/>
|
||||
</path>
|
||||
</svg>
|
After Width: | Height: | Size: 970 B |
5
app/frontend/src/assets/icon_success.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<svg width="106" height="106" viewBox="0 0 106 106" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="53" cy="53" r="53" fill="#40D8A1"/>
|
||||
<path d="M29 52.5L47.5 70.5" stroke="white" stroke-width="10" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<line x1="79" y1="40.0711" x2="48.0711" y2="71" stroke="white" stroke-width="10" stroke-linecap="round"/>
|
||||
</svg>
|
After Width: | Height: | Size: 377 B |
6
app/frontend/src/assets/icon_usb.svg
Normal file
@ -0,0 +1,6 @@
|
||||
<svg width="548" height="199" viewBox="0 0 548 199" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="403" y="27" width="142" height="145" rx="8" fill="white" stroke="black" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M0 30C0 13.4315 13.4315 0 30 0H428C432.418 0 436 3.58172 436 8V191C436 195.418 432.418 199 428 199H30C13.4315 199 0 185.569 0 169V30Z" fill="black"/>
|
||||
<rect x="477" y="55" width="26" height="26" fill="white" stroke="black" stroke-width="6"/>
|
||||
<rect x="477" y="117" width="26" height="26" fill="white" stroke="black" stroke-width="6"/>
|
||||
</svg>
|
After Width: | Height: | Size: 602 B |
11
app/frontend/src/assets/loading.svg
Normal file
@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="margin: auto; background: none; display: block; shape-rendering: auto;" width="200px" height="200px" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid">
|
||||
<circle cx="50" cy="50" r="0" fill="none" stroke="#f3f3f3" stroke-width="2">
|
||||
<animate attributeName="r" repeatCount="indefinite" dur="1.4925373134328357s" values="0;30" keyTimes="0;1" keySplines="0 0.2 0.8 1" calcMode="spline" begin="-0.7462686567164178s"></animate>
|
||||
<animate attributeName="opacity" repeatCount="indefinite" dur="1.4925373134328357s" values="1;0" keyTimes="0;1" keySplines="0.2 0 0.8 1" calcMode="spline" begin="-0.7462686567164178s"></animate>
|
||||
</circle>
|
||||
<circle cx="50" cy="50" r="0" fill="none" stroke="#d8dddf" stroke-width="2">
|
||||
<animate attributeName="r" repeatCount="indefinite" dur="1.4925373134328357s" values="0;30" keyTimes="0;1" keySplines="0 0.2 0.8 1" calcMode="spline"></animate>
|
||||
<animate attributeName="opacity" repeatCount="indefinite" dur="1.4925373134328357s" values="1;0" keyTimes="0;1" keySplines="0.2 0 0.8 1" calcMode="spline"></animate>
|
||||
</circle>
|
||||
<!-- [ldio] generated by https://loading.io/ --></svg>
|
After Width: | Height: | Size: 1.2 KiB |
BIN
app/frontend/src/assets/logo.png
Normal file
After Width: | Height: | Size: 88 KiB |
1
app/frontend/src/assets/spectre.min.css
vendored
Normal file
10
app/frontend/src/main.js
Normal file
@ -0,0 +1,10 @@
|
||||
import Vue from 'vue'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
Vue.config.productionTip = true
|
||||
Vue.config.devtools = true
|
||||
|
||||
new Vue({
|
||||
router,
|
||||
render: h => h(App)
|
||||
}).$mount('#app')
|
62
app/frontend/src/router/index.js
Normal file
@ -0,0 +1,62 @@
|
||||
import Vue from 'vue'
|
||||
import VueRouter from 'vue-router'
|
||||
|
||||
|
||||
Vue.use(VueRouter)
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'loader',
|
||||
component: () => import('../views/splash-screen.vue'),
|
||||
props: true
|
||||
},
|
||||
{
|
||||
path: '/home',
|
||||
name: 'home',
|
||||
component: () => import('../views/home.vue'),
|
||||
props: true
|
||||
},
|
||||
{
|
||||
path: '/wifi-select',
|
||||
name: 'wifi-select',
|
||||
component: () => import('../views/wifi-select.vue'),
|
||||
props: true
|
||||
},
|
||||
{
|
||||
path: '/generate-ap',
|
||||
name: 'generate-ap',
|
||||
component: () => import('../views/generate-ap.vue'),
|
||||
props: true
|
||||
},
|
||||
{
|
||||
path: '/capture',
|
||||
name: 'capture',
|
||||
component: () => import('../views/capture.vue'),
|
||||
props: true
|
||||
},
|
||||
{
|
||||
path: '/save-capture',
|
||||
name: 'save-capture',
|
||||
component: () => import('../views/save-capture.vue'),
|
||||
props: true
|
||||
},
|
||||
{ path: '/analysis',
|
||||
name: 'analysis',
|
||||
component: () => import('../views/analysis.vue'),
|
||||
props: true
|
||||
},
|
||||
{ path: '/report',
|
||||
name: 'report',
|
||||
component: () => import('../views/report.vue'),
|
||||
props: true
|
||||
}
|
||||
]
|
||||
|
||||
const router = new VueRouter({
|
||||
mode: 'history',
|
||||
base: process.env.BASE_URL,
|
||||
routes
|
||||
})
|
||||
|
||||
export default router
|
60
app/frontend/src/views/SimpleKeyboard.vue
Normal file
@ -0,0 +1,60 @@
|
||||
<template>
|
||||
<div :class="keyboardClass"></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Keyboard from "simple-keyboard";
|
||||
import "simple-keyboard/build/css/index.css";
|
||||
|
||||
export default {
|
||||
name: "SimpleKeyboard",
|
||||
props: {
|
||||
keyboardClass: {
|
||||
default: "simple-keyboard",
|
||||
type: String
|
||||
},
|
||||
input: {
|
||||
type: String
|
||||
}
|
||||
},
|
||||
data: () => ({
|
||||
keyboard: null
|
||||
}),
|
||||
mounted() {
|
||||
this.keyboard = new Keyboard({
|
||||
onChange: this.onChange,
|
||||
onKeyPress: this.onKeyPress
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
onChange(input) {
|
||||
this.$emit("onChange", input);
|
||||
},
|
||||
onKeyPress(button) {
|
||||
this.$emit("onKeyPress", button);
|
||||
|
||||
/**
|
||||
* If you want to handle the shift and caps lock buttons
|
||||
*/
|
||||
if (button === "{shift}" || button === "{lock}") this.handleShift();
|
||||
},
|
||||
handleShift() {
|
||||
let currentLayout = this.keyboard.options.layoutName;
|
||||
let shiftToggle = currentLayout === "default" ? "shift" : "default";
|
||||
|
||||
this.keyboard.setOptions({
|
||||
layoutName: shiftToggle
|
||||
});
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
input(input) {
|
||||
this.keyboard.setInput(input);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||
<style scoped>
|
||||
</style>
|
70
app/frontend/src/views/analysis.vue
Normal file
@ -0,0 +1,70 @@
|
||||
<template>
|
||||
<div class="center">
|
||||
<div v-if="question">
|
||||
<p>Do you want to analyze the captured communications?</p>
|
||||
<div class="empty-action">
|
||||
<button class="btn" v-on:click="save_capture()">No, just save them</button> <button class="btn btn-primary" v-on:click="start_analysis()">Yes, let's do it</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="running">
|
||||
<img src="@/assets/loading.svg"/>
|
||||
<p class="legend" v-if="!long_waiting">Please wait during the analysis...</p>
|
||||
<p class="legend fade-in" v-if="long_waiting">Yes, it can take some time...</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import router from '../router'
|
||||
import axios from 'axios'
|
||||
|
||||
export default {
|
||||
name: 'analysis',
|
||||
data() {
|
||||
return {
|
||||
question: true,
|
||||
running: false,
|
||||
check_alerts: false,
|
||||
long_waiting: false
|
||||
}
|
||||
},
|
||||
props: {
|
||||
capture_token: String
|
||||
},
|
||||
methods: {
|
||||
start_analysis: function() {
|
||||
this.question = false
|
||||
this.running = true
|
||||
setTimeout(function () { this.long_waiting = true }.bind(this), 15000);
|
||||
axios.get(`/api/analysis/start/${this.capture_token}`, { timeout: 60000 })
|
||||
.then(response => {
|
||||
if(response.data.message == "Analysis started")
|
||||
this.check_alerts = setInterval(() => { this.get_alerts(); }, 500);
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error);
|
||||
});
|
||||
},
|
||||
get_alerts: function() {
|
||||
axios.get(`/api/analysis/report/${this.capture_token}`, { timeout: 60000 })
|
||||
.then(response => {
|
||||
if(response.data.message != "No report yet"){
|
||||
clearInterval(this.check_alerts);
|
||||
this.long_waiting = false;
|
||||
this.running = false;
|
||||
router.replace({ name: 'report', params: { alerts : response.data.alerts,
|
||||
device : response.data.device,
|
||||
capture_token : this.capture_token } });
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error);
|
||||
});
|
||||
},
|
||||
save_capture: function() {
|
||||
var capture_token = this.capture_token
|
||||
router.replace({ name: 'save-capture', params: { capture_token: capture_token } });
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
100
app/frontend/src/views/capture.vue
Normal file
@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<div class="capture-wrapper">
|
||||
<svg id="sparkline" stroke-width="3" :width="sparkwidth" :height="sparkheight" v-if="sparklines"></svg>
|
||||
<div class="center">
|
||||
<div class="footer">
|
||||
<h3 class="timer">{{timer_hours}}:{{timer_minutes}}:{{timer_seconds}}</h3>
|
||||
<p>Intercepting the communications of {{device_name}}.</p>
|
||||
<div class="empty-action">
|
||||
<button class="btn" :class="[ loading ? 'loading' : 'btn-primary', ]" v-on:click="stop_capture()">Stop the capture</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
import router from '../router'
|
||||
import sparkline from '@fnando/sparkline'
|
||||
|
||||
export default {
|
||||
name: 'capture',
|
||||
components: {},
|
||||
data() {
|
||||
return {
|
||||
timer_hours: "00",
|
||||
timer_minutes: "00",
|
||||
timer_seconds: "00",
|
||||
loading: false,
|
||||
stats_interval: false,
|
||||
chrono_interval: false,
|
||||
sparklines: false
|
||||
}
|
||||
},
|
||||
props: {
|
||||
capture_token: String,
|
||||
device_name: String
|
||||
},
|
||||
methods: {
|
||||
set_chrono: function() {
|
||||
this.chrono_interval = setInterval(() => { this.chrono(); }, 10);
|
||||
},
|
||||
stop_capture: function() {
|
||||
this.loading = true
|
||||
axios.get(`/api/network/ap/stop`, { timeout: 30000 })
|
||||
axios.get(`/api/capture/stop`, { timeout: 30000 })
|
||||
.then(response => (this.handle_finish(response.data)))
|
||||
},
|
||||
get_stats: function() {
|
||||
axios.get(`/api/capture/stats`, { timeout: 30000 })
|
||||
.then(response => (this.handle_stats(response.data)))
|
||||
},
|
||||
handle_stats: function(data) {
|
||||
if (data.packets.length) sparkline(document.querySelector("#sparkline"), data.packets);
|
||||
},
|
||||
handle_finish: function(data) {
|
||||
clearInterval(this.chrono_interval);
|
||||
clearInterval(this.stats_interval);
|
||||
if (data.status) {
|
||||
this.loading = false
|
||||
var capture_token = this.capture_token
|
||||
router.replace({ name: 'analysis', params: { capture_token: capture_token } });
|
||||
}
|
||||
},
|
||||
chrono: function() {
|
||||
var time = Date.now() - this.capture_start
|
||||
this.timer_hours = Math.floor(time / (60 * 60 * 1000));
|
||||
this.timer_hours = (this.timer_hours < 10) ? "0" + this.timer_hours : this.timer_hours
|
||||
time = time % (60 * 60 * 1000);
|
||||
this.timer_minutes = Math.floor(time / (60 * 1000));
|
||||
this.timer_minutes = (this.timer_minutes < 10) ? "0" + this.timer_minutes : this.timer_minutes
|
||||
time = time % (60 * 1000);
|
||||
this.timer_seconds = Math.floor(time / 1000);
|
||||
this.timer_seconds = (this.timer_seconds < 10) ? "0" + this.timer_seconds : this.timer_seconds
|
||||
},
|
||||
setup_sparklines: function() {
|
||||
axios.get(`/api/misc/config`, { timeout: 60000 })
|
||||
.then(response => {
|
||||
if(response.data.sparklines){
|
||||
this.sparklines = true
|
||||
this.sparkwidth = window.screen.width + "px";
|
||||
this.sparkheight = Math.trunc(window.screen.height / 5) + "px";
|
||||
this.stats_interval = setInterval(() => { this.get_stats(); }, 500);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error)
|
||||
});
|
||||
}
|
||||
},
|
||||
created: function() {
|
||||
// Get the config for the sparklines.
|
||||
this.setup_sparklines()
|
||||
|
||||
// Start the chrono and get the first stats.
|
||||
this.capture_start = Date.now()
|
||||
this.set_chrono();
|
||||
}
|
||||
}
|
||||
</script>
|
119
app/frontend/src/views/generate-ap.vue
Normal file
@ -0,0 +1,119 @@
|
||||
<template>
|
||||
<div class="center">
|
||||
<div v-if="(error == false)">
|
||||
<div v-if="ssid_name">
|
||||
<div class="card apcard" v-on:click="generate_ap()">
|
||||
<div class="columns">
|
||||
<div class="column col-5">
|
||||
<center><img :src="ssid_qr" id="qrcode"></center>
|
||||
</div>
|
||||
<div class="divider-vert white-bg" data-content="OR"></div>
|
||||
<div class="column col-5"><br />
|
||||
<span class="light-grey">Network name: </span><br />
|
||||
<h4>{{ ssid_name }}</h4>
|
||||
<span class="light-grey">Network password: </span><br />
|
||||
<h4>{{ ssid_password }}</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<br /><br /><br /><br /> <br /><br /><br /><br /><br /><br />
|
||||
<!-- Requite a CSS MEME for that shit :) -->
|
||||
<span class="legend">Tap the white frame to generate a new network.</span>
|
||||
</div>
|
||||
<div v-else>
|
||||
<img src="@/assets/loading.svg"/>
|
||||
<p class="legend">We generate an ephemeral network for you.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<p>
|
||||
<strong>Unfortunately, we got some issues.</strong>
|
||||
<br /><br />
|
||||
Please verify that you've two Wifi interfaces on your device<br />
|
||||
and restart it by clicking on the button below.<br />
|
||||
</p>
|
||||
<button class="btn" v-on:click="reboot()">Reboot the device</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
import router from '../router'
|
||||
|
||||
export default {
|
||||
name: 'generate-ap',
|
||||
components: {},
|
||||
data() {
|
||||
return {
|
||||
ssid_name: false,
|
||||
ssid_qr: false,
|
||||
ssid_password: false,
|
||||
capture_token: false,
|
||||
capture_start: false,
|
||||
interval: false,
|
||||
error: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
generate_ap: function() {
|
||||
clearInterval(this.interval)
|
||||
this.ssid_name = false
|
||||
axios.get(`/api/network/ap/start`, { timeout: 30000 })
|
||||
.then(response => (this.show_ap(response.data)))
|
||||
},
|
||||
show_ap: function(data) {
|
||||
if (data.status) {
|
||||
this.ssid_name = data.ssid
|
||||
this.ssid_password = data.password
|
||||
this.ssid_qr = data.qrcode
|
||||
this.start_capture() // Start the capture before client connect.
|
||||
} else {
|
||||
this.error = true
|
||||
}
|
||||
},
|
||||
start_capture: function() {
|
||||
axios.get(`/api/capture/start`, { timeout: 30000 })
|
||||
.then(response => (this.get_capture_token(response.data)))
|
||||
},
|
||||
reboot: function() {
|
||||
axios.get(`/api/misc/reboot`, { timeout: 30000 })
|
||||
.then(response => { console.log(response)})
|
||||
},
|
||||
get_capture_token: function(data) {
|
||||
if (data.status) {
|
||||
this.capture_token = data.capture_token
|
||||
this.capture_start = Date.now()
|
||||
this.get_device()
|
||||
}
|
||||
},
|
||||
get_device: function() {
|
||||
this.interval = setInterval(() => {
|
||||
axios.get(`/api/device/get/${this.capture_token}`, { timeout: 30000 })
|
||||
.then(response => (this.check_device(response.data)))
|
||||
}, 500);
|
||||
},
|
||||
check_device: function(data) {
|
||||
if (data.status) {
|
||||
clearInterval(this.interval);
|
||||
var capture_token = this.capture_token
|
||||
var capture_start = this.capture_start
|
||||
var device_name = data.name
|
||||
router.replace({
|
||||
name: 'capture',
|
||||
params: {
|
||||
capture_token: capture_token,
|
||||
capture_start: capture_start,
|
||||
device_name: device_name
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
created: function() {
|
||||
this.generate_ap();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
24
app/frontend/src/views/home.vue
Normal file
@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<div class="center">
|
||||
<h3 class="lobster">Welcome to TinyCheck.</h3>
|
||||
<p>We are going to help you to check your device.</p>
|
||||
<button class="btn btn-primary" v-on:click="next()">Let's start!</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import router from '../router'
|
||||
|
||||
export default {
|
||||
name: 'home',
|
||||
props: { saved_ssid: String, list_ssids: Array, internet: Boolean },
|
||||
methods: {
|
||||
next: function() {
|
||||
var saved_ssid = this.saved_ssid
|
||||
var list_ssids = this.list_ssids
|
||||
var internet = this.internet
|
||||
router.push({ name: 'wifi-select', params: { saved_ssid: saved_ssid, list_ssids: list_ssids, internet:internet } });
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
115
app/frontend/src/views/report.vue
Normal file
@ -0,0 +1,115 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="results">
|
||||
<div v-if="alerts.high.length >= 1" class="high-wrapper">
|
||||
<div class="center">
|
||||
<h1 class="warning-title">You have {{ nb_translate(alerts.high.length) }} high alert,<br />your device seems to be compromised.</h1>
|
||||
<button class="btn btn-report-low-light" v-on:click="new_capture()">Start a new capture</button>
|
||||
<button class="btn btn-report-high" @click="show_report=true;results=false;">Show the full report</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="alerts.moderate.length >= 1" class="med-wrapper">
|
||||
<div class="center">
|
||||
<h1 class="warning-title">You have {{ nb_translate(alerts.moderate.length) }} moderate alerts,<br />your device might be compromised.</h1>
|
||||
<button class="btn btn-report-low-light" v-on:click="new_capture()">Start a new capture</button>
|
||||
<button class="btn btn-report-moderate" @click="show_report=true;results=false;">Show the full report</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="alerts.low.length >= 1" class="low-wrapper">
|
||||
<div class="center">
|
||||
<h1 class="warning-title">You have ony {{ nb_translate(alerts.moderate.low) }} low alerts,<br /> don't hesitate to check them.</h1>
|
||||
<button class="btn btn-report-low-light" v-on:click="new_capture()">Start a new capture</button>
|
||||
<button class="btn btn-report-low" @click="show_report=true;results=false;">Show the full report</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="none-wrapper">
|
||||
<div class="center">
|
||||
<h1 class="warning-title">Everything looks fine, zero alerts.</h1>
|
||||
<button class="btn btn-report-low-light" v-on:click="save_capture()">Save the capture</button>
|
||||
<button class="btn btn-report-low" v-on:click="new_capture()">Start a new capture</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="show_report" class="report-wrapper">
|
||||
<div class="device-ctx">
|
||||
<h3 style="margin: 0;">Report for {{device.name}}</h3>
|
||||
IP Address: {{device.ip_address}}<br />Mac Address: {{device.mac_address}}
|
||||
</div>
|
||||
<ul class="alerts">
|
||||
<li class="alert" v-for="alert in alerts.high" :key="alert.message">
|
||||
<span class="high-label">High</span><span class="alert-id">{{ alert.id }}</span>
|
||||
<div class="alert-body">
|
||||
<span class="title">{{ alert.title }}</span>
|
||||
<p class="description">{{ alert.description }}</p>
|
||||
</div>
|
||||
</li>
|
||||
<li class="alert" v-for="alert in alerts.moderate" :key="alert.message">
|
||||
<span class="moderate-label">Moderate</span><span class="alert-id">{{ alert.id }}</span>
|
||||
<div class="alert-body">
|
||||
<span class="title">{{ alert.title }}</span>
|
||||
<p class="description">{{ alert.description }}</p>
|
||||
</div>
|
||||
</li>
|
||||
<li class="alert" v-for="alert in alerts.low" :key="alert.message">
|
||||
<span class="low-label">Low</span><span class="alert-id">{{ alert.id }}</span>
|
||||
<div class="alert-body">
|
||||
<span class="title">{{ alert.title }}</span>
|
||||
<p class="description">{{ alert.description }}</p>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="columns" id="controls-analysis">
|
||||
<div class="column col-5">
|
||||
<button class="btn width-100" @click="$router.push('generate-ap')">Start a capture</button>
|
||||
</div>
|
||||
<div class="divider-vert column col-2" data-content="OR"></div>
|
||||
<div class="column col-5">
|
||||
<button class="btn btn btn-primary width-100" v-on:click="save_capture()">Save the report</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<style>
|
||||
#app {
|
||||
overflow-y: visible;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import router from '../router'
|
||||
|
||||
export default {
|
||||
name: 'report',
|
||||
data() {
|
||||
return {
|
||||
results: true,
|
||||
}
|
||||
},
|
||||
props: {
|
||||
device: Object,
|
||||
alerts: Array,
|
||||
capture_token: String
|
||||
},
|
||||
methods: {
|
||||
save_capture: function() {
|
||||
var capture_token = this.capture_token
|
||||
router.replace({ name: 'save-capture', params: { capture_token: capture_token } });
|
||||
},
|
||||
new_capture: function() {
|
||||
router.push({ name: 'generate-ap' })
|
||||
},
|
||||
nb_translate: function(x) {
|
||||
var nbs = ['zero','one','two','three','four', 'five','six','seven','eight','nine', 'ten', 'eleven']
|
||||
try {
|
||||
return nbs[x];
|
||||
} catch (error)
|
||||
{
|
||||
return x;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
200
app/frontend/src/views/save-capture.vue
Normal file
@ -0,0 +1,200 @@
|
||||
<template>
|
||||
<div class="center" v-if="save_usb && init">
|
||||
<div class="canvas-anim" :class="{'anim-connect': !saved && !usb}" v-on:click="new_capture()">
|
||||
<div class="icon-spinner" v-if="!saved && usb"></div>
|
||||
<div class="icon-success" v-if="saved"></div>
|
||||
<div class="icon-usb"></div>
|
||||
<div class="icon-usb-plug"></div>
|
||||
</div>
|
||||
<p class="legend" v-if="!saved && !usb"><br />Please connect a USB key to save your capture.</p>
|
||||
<p class="legend" v-if="!saved && usb"><br />We are saving your capture.</p>
|
||||
<p class="legend" v-if="saved"><br />You can tap the USB key to start a new capture.</p>
|
||||
</div>
|
||||
<div class="center" v-else-if="!save_usb && init">
|
||||
<div>
|
||||
<p class="legend">The capture download is going to start...<br /><br /><br /></p>
|
||||
<button class="btn btn-primary" v-on:click="new_capture()">Start another capture</button>
|
||||
<iframe :src="download_url" class="frame-download"></iframe>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
.canvas-anim {
|
||||
height: 120px;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
width: 205px;
|
||||
|
||||
&.anim-connect {
|
||||
width: 300px;
|
||||
|
||||
.icon-usb {
|
||||
-webkit-animation: slide-right 1s cubic-bezier(0.455, 0.030, 0.515, 0.955) infinite alternate both;
|
||||
animation: slide-right 1s cubic-bezier(0.455, 0.030, 0.515, 0.955) infinite alternate both;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.icon-usb {
|
||||
background: url('../assets/icon_usb.svg') no-repeat 0 0;
|
||||
background-size: 200px auto;
|
||||
display: block;
|
||||
height: 120px;
|
||||
position: absolute;
|
||||
top: 25px;
|
||||
left: 0;
|
||||
width: 200px;
|
||||
z-index: 8;
|
||||
}
|
||||
|
||||
.icon-usb-plug {
|
||||
background: url('../assets/icon_plug_usb.svg') no-repeat 0 0;
|
||||
background-size: cover;
|
||||
display: block;
|
||||
height: 120px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: -10px;
|
||||
width: 55px;
|
||||
z-index: 9;
|
||||
}
|
||||
|
||||
.icon-success {
|
||||
background: url('../assets/icon_success.svg') no-repeat 0 0;
|
||||
background-size: 80px auto;
|
||||
display: block;
|
||||
position: absolute;
|
||||
height: 120px;
|
||||
top: -25px;
|
||||
left: -40px;
|
||||
width: 80px;
|
||||
z-index: 10;
|
||||
-webkit-animation: scale-down-center 0.7s cubic-bezier(0.250, 0.460, 0.450, 0.940) both;
|
||||
animation: scale-down-center 0.7s cubic-bezier(0.250, 0.460, 0.450, 0.940) both;
|
||||
}
|
||||
|
||||
.icon-spinner {
|
||||
background: url('../assets/icon_spinner.svg') no-repeat 0 0;
|
||||
background-color: #f7f8f9;
|
||||
border-radius: 40px;
|
||||
display: block;
|
||||
height: 40px;
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
left: -20px;
|
||||
width: 40px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
@-webkit-keyframes slide-right {
|
||||
0% {
|
||||
-webkit-transform: translateX(0);
|
||||
transform: translateX(0);
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: translateX(75px);
|
||||
transform: translateX(75px);
|
||||
}
|
||||
}
|
||||
@keyframes slide-right {
|
||||
0% {
|
||||
-webkit-transform: translateX(0);
|
||||
transform: translateX(0);
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: translateX(75px);
|
||||
transform: translateX(75px);
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes scale-down-center {
|
||||
0% {
|
||||
-webkit-transform: scale(1);
|
||||
transform: scale(1);
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: scale(0.5);
|
||||
transform: scale(0.5);
|
||||
}
|
||||
}
|
||||
@keyframes scale-down-center {
|
||||
0% {
|
||||
-webkit-transform: scale(1);
|
||||
transform: scale(1);
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: scale(0.5);
|
||||
transform: scale(0.5);
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
import router from '../router'
|
||||
|
||||
export default {
|
||||
name: 'save-capture',
|
||||
components: {},
|
||||
data() {
|
||||
return {
|
||||
usb: false,
|
||||
saved: false,
|
||||
save_usb: false,
|
||||
init: false
|
||||
}
|
||||
},
|
||||
props: {
|
||||
capture_token: String
|
||||
},
|
||||
methods: {
|
||||
check_usb: function() {
|
||||
axios.get(`/api/save/usb-check`, { timeout: 30000 })
|
||||
.then(response => {
|
||||
if(response.data.status) {
|
||||
this.usb = true
|
||||
clearInterval(this.interval)
|
||||
this.save_capture()
|
||||
}
|
||||
})
|
||||
},
|
||||
save_capture: function() {
|
||||
var capture_token = this.capture_token
|
||||
axios.get(`/api/save/save-capture/${capture_token}/usb`, { timeout: 30000 })
|
||||
.then(response => {
|
||||
if(response.data.status){
|
||||
this.saved = true
|
||||
this.timeout = setTimeout(() => router.push('/'), 60000);
|
||||
}
|
||||
})
|
||||
},
|
||||
new_capture: function() {
|
||||
clearTimeout(this.timeout);
|
||||
router.push({ name: 'generate-ap' })
|
||||
},
|
||||
load_config: function() {
|
||||
axios.get(`/api/misc/config`, { timeout: 60000 })
|
||||
.then(response => {
|
||||
if(response.data.download_links){
|
||||
this.init = true
|
||||
this.save_usb = false
|
||||
this.download_url = `/api/save/save-capture/${this.capture_token}/url`
|
||||
} else {
|
||||
this.init = true
|
||||
this.save_usb = true
|
||||
this.interval = setInterval(() => { this.check_usb() }, 500);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error)
|
||||
});
|
||||
}
|
||||
},
|
||||
created: function() {
|
||||
this.load_config()
|
||||
}
|
||||
}
|
||||
</script>
|
53
app/frontend/src/views/splash-screen.vue
Normal file
@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<div class="center">
|
||||
<img src="@/assets/logo.png" id="tinycheck-logo" />
|
||||
<div class="loading loading-lg loadingsplash"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import router from '../router'
|
||||
import axios from 'axios'
|
||||
|
||||
export default {
|
||||
name: 'splash-screen',
|
||||
components: {},
|
||||
data() {
|
||||
return {
|
||||
list_ssids: [],
|
||||
internet: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// Check if the device is already connected to internet.
|
||||
internet_check: function() {
|
||||
axios.get(`/api/network/status`, { timeout: 10000 })
|
||||
.then(response => {
|
||||
if (response.data.internet) this.internet = true
|
||||
this.get_wifi_networks()
|
||||
})
|
||||
.catch(err => (console.log(err)))
|
||||
},
|
||||
// Get the WiFi networks around the box.
|
||||
get_wifi_networks: function() {
|
||||
axios.get(`/api/network/wifi/list`, { timeout: 10000 })
|
||||
.then(response => (this.append_ssids(response.data.networks)))
|
||||
.catch(err => (console.log(err)))
|
||||
},
|
||||
// Handle the get_wifi_networks answer and call goto_home.
|
||||
append_ssids: function(networks) {
|
||||
this.list_ssids = networks
|
||||
this.goto_home()
|
||||
},
|
||||
// Pass the list of ssids and the internet status as a prop to the home view.
|
||||
goto_home: function() {
|
||||
var list_ssids = this.list_ssids
|
||||
var internet = this.internet
|
||||
router.replace({ name: 'home', params: { list_ssids: list_ssids, internet: internet } });
|
||||
}
|
||||
},
|
||||
created: function() {
|
||||
this.internet_check();
|
||||
}
|
||||
}
|
||||
</script>
|
166
app/frontend/src/views/wifi-select.vue
Normal file
@ -0,0 +1,166 @@
|
||||
<template>
|
||||
<div :class="[ keyboard == false ? 'center' : '' ]">
|
||||
<div v-if="keyboard == false">
|
||||
<div v-if="have_internet">
|
||||
<p>You seem to be already connected to a network.<br />Do you want to use the current connection?</p>
|
||||
<div class="empty-action">
|
||||
<button class="btn" @click="have_internet = false">No, use another</button> <button class="btn" :class="[ connecting ? 'loading' : '', success ? 'btn-success' : 'btn-primary', ]" @click="$router.push({ name: 'generate-ap' })">Yes, use it.</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div v-if="enter_creds" class="wifi-login">
|
||||
<div class="form-group">
|
||||
<select class="form-select" id="ssid-select" v-model="ssid">
|
||||
<option value="" selected>Wifi name</option>
|
||||
<option v-for="ssid in ssids" v-bind:key="ssid.ssid">
|
||||
{{ ssid.ssid }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input class="form-input" type="password" id="password" v-model="password" placeholder="Wifi password" v-on:click="keyboard = (virtual_keyboard)? true : false">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<button class="btn width-100" :class="[ connecting ? 'loading' : '', success ? 'btn-success' : 'btn-primary', ]" v-on:click="wifi_setup()">{{ btnval }}</button>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<button class="btn width-100" :class="[ refreshing ? 'loading' : '' ]" v-on:click="refresh_wifi_list()">Refresh networks list</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<p><strong>You seem to not be connected to Internet.</strong><br />Please configure the Wi-Fi connection.</p>
|
||||
<div class="empty-action">
|
||||
<button class="btn btn-primary" @click="enter_creds = true">Ok, let's do that.</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<input :value="input" class="keyboardinput" @input="onInputChange" placeholder="Tap on the virtual keyboard to start">
|
||||
<SimpleKeyboard @onChange="onChange" @onKeyPress="onKeyPress" :input="input" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
#app {
|
||||
overflow-y: hidden;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
import router from '../router'
|
||||
import SimpleKeyboard from "./SimpleKeyboard";
|
||||
|
||||
export default {
|
||||
name: 'wifi-select',
|
||||
components: {
|
||||
SimpleKeyboard
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
connecting: false,
|
||||
error: false,
|
||||
success: false,
|
||||
btnval: "Connect to it.",
|
||||
ssid: "",
|
||||
selected_ssid: false,
|
||||
password: "",
|
||||
keyboard: false,
|
||||
input: "",
|
||||
ssids: [],
|
||||
have_internet: false,
|
||||
enter_creds: false,
|
||||
virtual_keyboard: false,
|
||||
refreshing: false
|
||||
}
|
||||
},
|
||||
props: {
|
||||
saved_ssid: String,
|
||||
list_ssids: Array,
|
||||
internet: Boolean
|
||||
},
|
||||
methods: {
|
||||
wifi_connect: function() {
|
||||
axios.get(`/api/network/wifi/connect`, { timeout: 60000 })
|
||||
.then(response => {
|
||||
if (response.data.status) {
|
||||
this.success = true
|
||||
this.connecting = false
|
||||
this.btnval = "Wifi connected!"
|
||||
setTimeout(() => router.push('generate-ap'), 1000);
|
||||
} else {
|
||||
this.btnval = "Wifi not connected. Please retry."
|
||||
this.connecting = false
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error)
|
||||
});
|
||||
},
|
||||
wifi_setup: function() {
|
||||
if (this.ssid.length && this.password.length >= 8 ){
|
||||
axios.post(`/api/network/wifi/setup`, { ssid: this.ssid, password: this.password }, { timeout: 60000 })
|
||||
.then(response => {
|
||||
if(response.data.status) {
|
||||
this.connecting = true
|
||||
this.wifi_connect()
|
||||
} else {
|
||||
console.log(response.data.message)
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error)
|
||||
});
|
||||
}
|
||||
},
|
||||
load_config: function() {
|
||||
axios.get(`/api/misc/config`, { timeout: 60000 })
|
||||
.then(response => {
|
||||
this.virtual_keyboard = response.data.virtual_keyboard
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error)
|
||||
});
|
||||
},
|
||||
onChange(input) {
|
||||
this.input = input
|
||||
this.password = this.input;
|
||||
},
|
||||
onKeyPress(button) {
|
||||
if (button == "{enter}")
|
||||
this.keyboard = false
|
||||
},
|
||||
onInputChange(input) {
|
||||
this.input = input.target.value;
|
||||
},
|
||||
append_ssids: function(networks) {
|
||||
this.ssids = networks
|
||||
},
|
||||
refresh_wifi_list: function(){
|
||||
this.refreshing = true
|
||||
axios.get(`/api/network/wifi/list`, { timeout: 10000 })
|
||||
.then(response => {
|
||||
this.refreshing = false
|
||||
this.append_ssids(response.data.networks)
|
||||
}).catch(error => {
|
||||
this.refreshing = false
|
||||
console.log(error)
|
||||
});
|
||||
}
|
||||
},
|
||||
created: function() {
|
||||
this.load_config()
|
||||
|
||||
this.have_internet = (this.internet) ? true : false
|
||||
this.keyboard = false
|
||||
|
||||
if (typeof this.list_ssids == "object" && this.list_ssids.length != 0){
|
||||
this.ssids = this.list_ssids
|
||||
} else {
|
||||
this.refresh_wifi_list()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
11
app/frontend/vue.config.js
Normal file
@ -0,0 +1,11 @@
|
||||
module.exports = {
|
||||
devServer: {
|
||||
proxy: {
|
||||
'^/api': {
|
||||
target: 'http://localhost:8040',
|
||||
ws: true,
|
||||
changeOrigin: true
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
BIN
assets/backend.png
Normal file
After Width: | Height: | Size: 775 KiB |
BIN
assets/frontend.png
Normal file
After Width: | Height: | Size: 405 KiB |
1
assets/iocs.json
Normal file
BIN
assets/network-home.png
Normal file
After Width: | Height: | Size: 315 KiB |
15
assets/requirements.txt
Normal file
@ -0,0 +1,15 @@
|
||||
ipwhois
|
||||
M2Crypto
|
||||
pyOpenSSL
|
||||
pydig
|
||||
netaddr
|
||||
pyyaml
|
||||
flask
|
||||
flask_httpauth
|
||||
pyjwt
|
||||
sqlalchemy
|
||||
psutil
|
||||
pyudev
|
||||
wifi
|
||||
qrcode
|
||||
netifaces
|
19
assets/scheme.sql
Normal file
@ -0,0 +1,19 @@
|
||||
CREATE TABLE "iocs" (
|
||||
"id" INTEGER UNIQUE,
|
||||
"value" TEXT NOT NULL,
|
||||
"type" TEXT NOT NULL,
|
||||
"tlp" TEXT NOT NULL,
|
||||
"tag" TEXT NOT NULL,
|
||||
"source" TEXT NOT NULL,
|
||||
"added_on" NUMERIC NOT NULL,
|
||||
PRIMARY KEY("id" AUTOINCREMENT)
|
||||
);
|
||||
|
||||
CREATE TABLE "whitelist" (
|
||||
"id" INTEGER UNIQUE,
|
||||
"element" TEXT NOT NULL UNIQUE,
|
||||
"type" TEXT NOT NULL,
|
||||
"source" TEXT NOT NULL,
|
||||
"added_on" INTEGER NOT NULL,
|
||||
PRIMARY KEY("id" AUTOINCREMENT)
|
||||
);
|
1
assets/whitelist.json
Normal file
73
config.yaml
Normal file
@ -0,0 +1,73 @@
|
||||
# ANALYSIS -
|
||||
# All the things related to the analysis engine.
|
||||
# Some configuration keys aren't accessible from the backend
|
||||
# but can be edited here...
|
||||
#
|
||||
analysis:
|
||||
free_issuers:
|
||||
- CN=Let's Encrypt Authority X3,O=Let's Encrypt,C=US
|
||||
- CN=ZeroSSL RSA Domain Secure Site CA,O=ZeroSSL,C=AT
|
||||
heuristics: true
|
||||
http_default_port: 80
|
||||
iocs: true
|
||||
max_alerts: 3
|
||||
max_ports: 1024
|
||||
remote: false
|
||||
ssl_default_ports:
|
||||
- 443
|
||||
- 465
|
||||
- 636
|
||||
- 989
|
||||
- 990
|
||||
- 993
|
||||
- 995
|
||||
- 5223
|
||||
whitelist: true
|
||||
|
||||
# BACKEND -
|
||||
# Backend login / password and the possibility to
|
||||
# access to it from remote location.
|
||||
#
|
||||
backend:
|
||||
login: tinycheck
|
||||
password: 2de5a04967d6cffd33243bb226db194b97e1d6d1331eea3ad1e8c5e9f6e58315
|
||||
remote_access: true
|
||||
|
||||
# FRONTEND -
|
||||
# Some elements related to the frontend configuration & ergonomy
|
||||
# you can change them via the backend.
|
||||
#
|
||||
frontend:
|
||||
download_links: false
|
||||
hide_mouse: true
|
||||
kiosk_mode: true
|
||||
remote_access: false
|
||||
sparklines: true
|
||||
virtual_keyboard: true
|
||||
|
||||
# NETWORK -
|
||||
# Some elements related to the network configuration, such as
|
||||
# the interfaces (updated during the install), the list of SSIDs
|
||||
# the URL to check internet and the tokenization of SSIDs.
|
||||
#
|
||||
network:
|
||||
in: iface_in
|
||||
internet_check: http://example.com
|
||||
out: iface_out
|
||||
ssids:
|
||||
- wireless
|
||||
- skynet
|
||||
- network
|
||||
- wifi
|
||||
tokenized_ssids: true
|
||||
|
||||
# WATCHERS -
|
||||
# They are used to grab automatically new IOCs or whitelisted
|
||||
# elements from files containing IOCs export. You can add your
|
||||
# own URL and it will be parsed at each TinyCheck reboot.
|
||||
#
|
||||
watchers:
|
||||
iocs:
|
||||
- https://raw.githubusercontent.com/KasperskyLab/TinyCheck/assets/iocs.json
|
||||
whitelists:
|
||||
- https://raw.githubusercontent.com/KasperskyLab/TinyCheck/assets/whitelist.json
|
365
install.sh
Normal file
@ -0,0 +1,365 @@
|
||||
#!/bin/bash
|
||||
|
||||
ifaces=()
|
||||
rfaces=()
|
||||
CURRENT_USER="${SUDO_USER}"
|
||||
SCRIPT_PATH="$( cd "$(dirname "$0")" ; pwd -P )"
|
||||
|
||||
welcome_screen() {
|
||||
cat << "EOF"
|
||||
_____ _ ___ _ _
|
||||
/__ (_)_ __ _ _ / __\ |__ ___ ___| | __
|
||||
/ /\/ | '_ \| | | |/ / | '_ \ / _ \/ __| |/ /
|
||||
/ / | | | | | |_| / /___| | | | __/ (__| <
|
||||
\/ |_|_| |_|\__, \____/|_| |_|\___|\___|_|\_\
|
||||
|___/
|
||||
-----
|
||||
|
||||
EOF
|
||||
}
|
||||
|
||||
check_operating_system() {
|
||||
# Check that this installer is running on a
|
||||
# Debian-like operating system (for dependencies)
|
||||
|
||||
echo -e "\e[39m[+] Checking operating system\e[39m"
|
||||
error="\e[91m [✘] Need to be run on a Debian-like operating system, exiting.\e[39m"
|
||||
|
||||
if [[ -f "/etc/os-release" ]]; then
|
||||
if [[ $(cat /etc/os-release | grep "ID_LIKE=debian") ]]; then
|
||||
echo -e "\e[92m [✔] Debian-like operating system\e[39m"
|
||||
else
|
||||
echo -e "$error"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo -e "$error"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
check_connection() {
|
||||
# Checking internet connectivity to install
|
||||
# TinyCheck dependencies
|
||||
|
||||
echo -e "\e[39m[+] Checking internet connectivity to install dependencies\e[39m"
|
||||
if nc -zw1 example.com 443; then
|
||||
echo -e "\e[92m [✔] Internet link is connected\e[39m"
|
||||
else
|
||||
echo -e "\e[91m [✘] No internet connection, exiting.\e[39m"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
create_directory() {
|
||||
# Create the TinyCheck directory and move the whole stuff there.
|
||||
echo -e "[+] Creating TinyCheck folder under /usr/share/"
|
||||
mkdir /usr/share/tinycheck
|
||||
cp -Rf ./* /usr/share/tinycheck
|
||||
}
|
||||
|
||||
generate_certificate() {
|
||||
# Generating SSL certificate for the backend.
|
||||
echo -e "[+] Generating SSL certificate for the backend"
|
||||
openssl req -x509 -subj '/CN=tinycheck.local/O=TinyCheck Backend' -newkey rsa:4096 -nodes -keyout /usr/share/tinycheck/server/backend/key.pem -out /usr/share/tinycheck/server/backend/cert.pem -days 3650
|
||||
}
|
||||
|
||||
create_services() {
|
||||
# Create services to launch the two servers.
|
||||
|
||||
echo -e "\e[39m[+] Creating services\e[39m"
|
||||
|
||||
echo -e "\e[92m [✔] Creating frontend service\e[39m"
|
||||
cat >/lib/systemd/system/tinycheck-frontend.service <<EOL
|
||||
[Unit]
|
||||
Description=TinyCheck frontend service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/usr/bin/python3 /usr/share/tinycheck/server/frontend/main.py
|
||||
Restart=on-abort
|
||||
KillMode=process
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOL
|
||||
|
||||
echo -e "\e[92m [✔] Creating backend service\e[39m"
|
||||
cat >/lib/systemd/system/tinycheck-backend.service <<EOL
|
||||
[Unit]
|
||||
Description=TinyCheck frontend service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/usr/bin/python3 /usr/share/tinycheck/server/backend/main.py
|
||||
Restart=on-abort
|
||||
KillMode=process
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOL
|
||||
|
||||
echo -e "\e[92m [✔] Creating kiosk service\e[39m"
|
||||
cat >/lib/systemd/system/tinycheck-kiosk.service <<EOL
|
||||
[Unit]
|
||||
Description=TinyCheck Kiosk
|
||||
Wants=graphical.target
|
||||
After=graphical.target
|
||||
|
||||
[Service]
|
||||
Environment=DISPLAY=:0.0
|
||||
Environment=XAUTHORITY=/home/${CURRENT_USER}/.Xauthority
|
||||
Type=forking
|
||||
ExecStart=/bin/bash /usr/share/tinycheck/kiosk.sh
|
||||
Restart=on-abort
|
||||
User=${CURRENT_USER}
|
||||
Group=${CURRENT_USER}
|
||||
|
||||
[Install]
|
||||
WantedBy=graphical.target
|
||||
EOL
|
||||
|
||||
echo -e "\e[92m [✔] Creating watchers service\e[39m"
|
||||
cat >/lib/systemd/system/tinycheck-watchers.service <<EOL
|
||||
[Unit]
|
||||
Description=TinyCheck watchers service
|
||||
Wants=network-online.target
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/usr/bin/python3 /usr/share/tinycheck/server/backend/watchers.py
|
||||
Restart=on-abort
|
||||
KillMode=process
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOL
|
||||
|
||||
echo -e "\e[92m [✔] Enabling services\e[39m"
|
||||
systemctl enable tinycheck-frontend
|
||||
systemctl enable tinycheck-backend
|
||||
systemctl enable tinycheck-kiosk
|
||||
systemctl enable tinycheck-watchers
|
||||
}
|
||||
|
||||
configure_dnsmask() {
|
||||
# Configure DNSMASQ by appending few lines to its configuration.
|
||||
# It creates a small DHCP server for one device.
|
||||
|
||||
echo -e "\e[39m[+] Configuring dnsmasq\e[39m"
|
||||
echo -e "\e[92m [✔] Changing dnsmasq configuration\e[39m"
|
||||
rand=$(head /dev/urandom | tr -dc a-z | head -c 13)
|
||||
|
||||
if [[ -f "/etc/dnsmasq.conf" ]]; then
|
||||
cat >>/etc/dnsmasq.conf <<EOL
|
||||
|
||||
## TinyCheck configuration ##
|
||||
|
||||
interface=${ifaces[-1]}
|
||||
dhcp-range=192.168.100.2,192.168.100.3,255.255.255.0,24h
|
||||
domain=local
|
||||
address=/$rand.local/192.168.100.1
|
||||
EOL
|
||||
else
|
||||
echo -e "\e[91m [✘] /etc/dnsmasq.conf doesn't exist, configuration not updated.\e[39m"
|
||||
fi
|
||||
}
|
||||
|
||||
configure_dhcpcd() {
|
||||
# Configure DHCPCD by appending few lines to his configuration.
|
||||
# Allows to prevent the interface to stick to wpa_supplicant config.
|
||||
|
||||
echo -e "\e[39m[+] Configuring dhcpcd\e[39m"
|
||||
echo -e "\e[92m [✔] Changing dhcpcd configuration\e[39m"
|
||||
if [[ -f "/etc/dhcpcd.conf" ]]; then
|
||||
cat >>/etc/dhcpcd.conf <<EOL
|
||||
|
||||
## TinyCheck configuration ##
|
||||
|
||||
interface ${ifaces[-1]}
|
||||
static ip_address=192.168.100.1/24
|
||||
nohook wpa_supplicant
|
||||
EOL
|
||||
else
|
||||
echo -e "\e[91m [✘] /etc/dhcpcd.conf doesn't exist, configuration not updated.\e[39m"
|
||||
fi
|
||||
}
|
||||
|
||||
update_config(){
|
||||
# Update the configuration
|
||||
sed -i "s/iface_out/${ifaces[0]}/g" /usr/share/tinycheck/config.yaml
|
||||
sed -i "s/iface_in/${ifaces[-1]}/g" /usr/share/tinycheck/config.yaml
|
||||
}
|
||||
|
||||
change_hostname() {
|
||||
# Changing the hostname to tinycheck
|
||||
echo -e "[+] Changing the hostname to tinycheck"
|
||||
echo "tinycheck" > /etc/hostname
|
||||
sed -i 's/raspberrypi/tinycheck/g' /etc/hosts
|
||||
}
|
||||
|
||||
install_package() {
|
||||
# Install associated packages by using aptitude.
|
||||
if [[ $1 == "dnsmasq" || $1 == "hostapd" || $1 == "tshark" || $1 == "sqlite3" || $1 == "suricata" || $1 == "unclutter" ]]; then
|
||||
apt-get install $1 -y
|
||||
elif [[ $1 == "zeek" ]]; then
|
||||
distrib=$(cat /etc/os-release | grep -E "^ID=" | cut -d"=" -f2)
|
||||
version=$(cat /etc/os-release | grep "VERSION_ID" | cut -d"\"" -f2)
|
||||
if [[ $distrib == "debian" || $distrib == "ubuntu" ]]; then
|
||||
echo "deb http://download.opensuse.org/repositories/security:/zeek/Debian_$version/ /" > /etc/apt/sources.list.d/security:zeek.list
|
||||
wget -nv "https://download.opensuse.org/repositories/security:zeek/Debian_$version/Release.key" -O Release.key
|
||||
elif [[ $distrib == "raspbian" ]]; then
|
||||
echo "deb http://download.opensuse.org/repositories/security:/zeek/Raspbian_$version/ /" > /etc/apt/sources.list.d/security:zeek.list
|
||||
wget -nv "https://download.opensuse.org/repositories/security:zeek/Raspbian_$version/Release.key" -O Release.key
|
||||
fi
|
||||
apt-key add - < Release.key
|
||||
rm Release.key && sudo apt-get update
|
||||
apt-get install zeek -y
|
||||
elif [[ $1 == "nodejs" ]]; then
|
||||
curl -sL https://deb.nodesource.com/setup_12.x | bash
|
||||
apt-get install -y nodejs
|
||||
elif [[ $1 == "dig" ]]; then
|
||||
apt-get install -y dnsutils
|
||||
fi
|
||||
}
|
||||
|
||||
check_dependencies() {
|
||||
# Check binary dependencies associated to the project.
|
||||
# If not installed, call install_package with the package name.
|
||||
bins=("/usr/sbin/hostapd"
|
||||
"/usr/sbin/dnsmasq"
|
||||
"/opt/zeek/bin/zeek"
|
||||
"/usr/bin/tshark"
|
||||
"/usr/bin/dig"
|
||||
"/usr/bin/suricata"
|
||||
"/usr/bin/unclutter"
|
||||
"/usr/bin/sqlite3")
|
||||
|
||||
echo -e "\e[39m[+] Checking dependencies...\e[39m"
|
||||
for bin in "${bins[@]}"
|
||||
do
|
||||
if [[ -f "$bin" ]]; then
|
||||
echo -e "\e[92m [✔] ${bin##*/} installed\e[39m"
|
||||
else
|
||||
echo -e "\e[93m [✘] ${bin##*/} not installed, lets install it\e[39m"
|
||||
install_package ${bin##*/}
|
||||
fi
|
||||
done
|
||||
echo -e "\e[39m[+] Install NodeJS...\e[39m"
|
||||
install_package nodejs
|
||||
echo -e "\e[39m[+] Install Python packages...\e[39m"
|
||||
python3 -m pip install -r "$SCRIPT_PATH/assets/requirements.txt"
|
||||
}
|
||||
|
||||
compile_vuejs() {
|
||||
# Compile VueJS interfaces.
|
||||
echo -e "\e[39m[+] Compiling VueJS projects"
|
||||
cd /usr/share/tinycheck/app/backend/ && npm install && npm run build
|
||||
cd /usr/share/tinycheck/app/frontend/ && npm install && npm run build
|
||||
}
|
||||
|
||||
create_desktop() {
|
||||
# Create desktop icon to lauch TinyCheck in a browser
|
||||
echo -e "\e[39m[+] Create Desktop icon under /home/${CURRENT_USER}/Desktop\e[39m"
|
||||
cat >"/home/$CURRENT_USER/Desktop/tinycheck.desktop" <<EOL
|
||||
#!/usr/bin/env xdg-open
|
||||
|
||||
[Desktop Entry]
|
||||
Version=1.0
|
||||
Type=Application
|
||||
Terminal=false
|
||||
Exec=chromium-browser http://localhost
|
||||
Name=TinyCheck
|
||||
Comment=Launcher for the TinyCheck frontend
|
||||
Icon=/usr/share/tinycheck/app/frontend/src/assets/icon.png
|
||||
EOL
|
||||
}
|
||||
|
||||
cleaning() {
|
||||
# Removing some files and useless directories
|
||||
rm /usr/share/tinycheck/install.sh
|
||||
rm /usr/share/tinycheck/README.md
|
||||
rm /usr/share/tinycheck/LICENSE.txt
|
||||
rm /usr/share/tinycheck/NOTICE.txt
|
||||
rm -rf /usr/share/tinycheck/assets/
|
||||
|
||||
# Disabling the suricata service
|
||||
systemctl disable suricata.service &> /dev/null
|
||||
|
||||
# Removing some useless dependencies.
|
||||
sudo apt autoremove -y
|
||||
}
|
||||
|
||||
check_wlan_interfaces() {
|
||||
# Check the presence of two wireless interfaces by using rfkill.
|
||||
# Check if they are recognized by ifconfig, if not unblock them with rfkill.
|
||||
echo -e "\e[39m[+] Checking your wireless interfaces"
|
||||
|
||||
for iface in $(ifconfig | grep -oE wlan[0-9]); do ifaces+=("$iface"); done
|
||||
for iface in $(rfkill list | grep -oE phy[0-9]); do rfaces+=("$iface"); done
|
||||
|
||||
if [[ "${#rfaces[@]}" > 1 ]]; then
|
||||
echo -e "\e[92m [✔] Two interfaces detected, lets continue!\e[39m"
|
||||
if [[ "${#ifaces[@]}" < 1 ]]; then
|
||||
for iface in rfaces; do rfkill unblock "$iface"; done
|
||||
fi
|
||||
else
|
||||
echo -e "\e[91m [✘] Two wireless interfaces are required."
|
||||
echo -e " Please, plug a WiFi USB dongle and retry the install, exiting.\e[39m"
|
||||
exit
|
||||
fi
|
||||
}
|
||||
|
||||
create_database() {
|
||||
# Create the database under /usr/share/tinycheck/tinycheck.sqlite
|
||||
# This base will be provisioned in IOCs by the watchers
|
||||
sqlite3 "/usr/share/tinycheck/tinycheck.sqlite3" < "$SCRIPT_PATH/assets/scheme.sql"
|
||||
}
|
||||
|
||||
change_configs() {
|
||||
# Disable the autorun dialog from pcmanfm
|
||||
if [[ -f "/home/$CURRENT_USER/.config/pcmanfm/LXDE-pi/pcmanfm.conf" ]]; then
|
||||
sed -i 's/autorun=1/autorun=0/g' "/home/$CURRENT_USER/.config/pcmanfm/LXDE-pi/pcmanfm.conf"
|
||||
fi
|
||||
# Disable the .desktop script popup
|
||||
if [[ -f "/home/$CURRENT_USER/.config/libfm/libfm.conf" ]]; then
|
||||
sed -i 's/quick_exec=0/quick_exec=1/g' "/home/$CURRENT_USER/.config/libfm/libfm.conf"
|
||||
fi
|
||||
}
|
||||
|
||||
feeding_iocs() {
|
||||
echo -e "\e[39m[+] Feeding your TinyCheck instance with fresh IOCs and whitelist."
|
||||
python3 /usr/share/tinycheck/server/backend/watchers.py
|
||||
}
|
||||
|
||||
reboot_box() {
|
||||
echo -e "\e[92m[+] The system is going to reboot\e[39m"
|
||||
sleep 5
|
||||
reboot
|
||||
}
|
||||
|
||||
if [[ $EUID -ne 0 ]]; then
|
||||
echo "This must be run as root. Type in 'sudo bash $0' to run."
|
||||
exit 1
|
||||
else
|
||||
welcome_screen
|
||||
check_operating_system
|
||||
check_connection
|
||||
check_wlan_interfaces
|
||||
create_directory
|
||||
check_dependencies
|
||||
configure_dnsmask
|
||||
configure_dhcpcd
|
||||
update_config
|
||||
change_hostname
|
||||
generate_certificate
|
||||
compile_vuejs
|
||||
create_database
|
||||
create_services
|
||||
create_desktop
|
||||
change_configs
|
||||
feeding_iocs
|
||||
cleaning
|
||||
reboot_box
|
||||
fi
|
19
kiosk.sh
Normal file
@ -0,0 +1,19 @@
|
||||
#!/bin/bash
|
||||
|
||||
# This small script is started by the service tinycheck-kiosk
|
||||
# in order to launch TinyCheck frontend in kiosk mode.
|
||||
|
||||
xset s noblank
|
||||
xset s off
|
||||
xset -dpms
|
||||
|
||||
if grep 'hide_mouse: true' /usr/share/tinycheck/config.yaml; then
|
||||
unclutter -idle 0 &
|
||||
fi
|
||||
|
||||
if grep 'kiosk_mode: true' /usr/share/tinycheck/config.yaml; then
|
||||
sed -i 's/"exited_cleanly":false/"exited_cleanly":true/' /home/pi/.config/chromium/Default/Preferences
|
||||
sed -i 's/"exit_type":"Crashed"/"exit_type":"Normal"/' /home/pi/.config/chromium/Default/Preferences
|
||||
|
||||
/usr/bin/chromium-browser http://127.0.0.1 --start-fullscreen --kiosk --incognito --noerrdialogs --disable-translate --no-first-run --fast --fast-start --disable-infobars --disable-features=TranslateUI &
|
||||
fi
|