First commit

This commit is contained in:
Félix Aime 2020-11-24 19:45:03 +01:00
parent c042b71634
commit 513f6b1b02
130 changed files with 76873 additions and 0 deletions

137
.gitignore vendored Normal file
View 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

File diff suppressed because one or more lines are too long

1
NOTICE.txt Normal file

File diff suppressed because one or more lines are too long

145
README.md Normal file
View 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
View File

61
analysis/analysis.py Normal file
View 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.")

View 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

View 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}]

View 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
View 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
View 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
View 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/).

View File

@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}

12010
app/backend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

44
app/backend/package.json Normal file
View 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"
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

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

View 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;
}

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 805 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 784 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 315 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

10
app/backend/src/main.js Normal file
View 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')

View 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

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

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

View 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">&hearts;</span> by <a href="https://twitter.com/felixaime" target="_blank">Félix Aimé</a>.</p>
</div>
</div>
</div>
</template>

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

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

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

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

View 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
View 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
View 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/).

View File

@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}

12248
app/frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

48
app/frontend/package.json Normal file
View 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"
]
}

View File

@ -0,0 +1,3 @@
{
"python.pythonPath": "/usr/local/opt/python@3.8/bin/python3.8"
}

30
app/frontend/src/App.vue Normal file
View 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>

View 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;
}
}

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 805 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 784 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View 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

View 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

View 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

View 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

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

File diff suppressed because one or more lines are too long

10
app/frontend/src/main.js Normal file
View 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')

View 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

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

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

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

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

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

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

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

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

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

View File

@ -0,0 +1,11 @@
module.exports = {
devServer: {
proxy: {
'^/api': {
target: 'http://localhost:8040',
ws: true,
changeOrigin: true
},
}
}
}

BIN
assets/backend.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 775 KiB

BIN
assets/frontend.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 405 KiB

1
assets/iocs.json Normal file

File diff suppressed because one or more lines are too long

BIN
assets/network-home.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 315 KiB

15
assets/requirements.txt Normal file
View 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
View 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

File diff suppressed because one or more lines are too long

73
config.yaml Normal file
View 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
View 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
View 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

Some files were not shown because too many files have changed in this diff Show More